beemdevelopment/Aegis · critical · IOException

Attempted to write outside of the icon pack directory

Error message

Attempted to write outside of the icon pack directory

What it means

For each icon declared in pack.json, importPack computes the destination file inside the pack directory and asserts its canonical path stays within that directory. If an icon's relative filename escapes the pack dir (e.g. "../../vault.json"), the import aborts with this IOException. Like the parent-directory check, this blocks ZIP-slip attacks on individual icon entries.

Solutions

  1. Do not import icon packs from untrusted sources — this indicates a malicious or corrupted pack.json
  2. Inspect the "filename"/relative path fields in pack.json for ../ or absolute path segments
  3. Fix the icon filenames to plain relative names and repackage the ZIP
  4. Re-download the pack from a trusted source

Example fix

// before (pack.json)
{ "filename": "../../../databases/aegis.db" }
// after (pack.json)
{ "filename": "icon.png" }
Defensive patterns

Strategy: validation

Validate before calling

File destFile = new File(packDir, icon.getRelativeFilename());
if (!destFile.getCanonicalPath().startsWith(packDir.getCanonicalPath() + File.separator)) {
    throw new IconPackException("Unsafe icon path: " + icon.getRelativeFilename());
}

Type guard

boolean isSafeRelativeName(String name) {
    return name != null && !name.startsWith("/") && !name.contains("..") && !name.contains("\\");
}

Try / catch

try {
    iconPackManager.importPack(file);
} catch (IconPackException e) {
    Log.e(TAG, "Icon pack rejected (path traversal in icon filename)", e);
}

Prevention

When it happens

Trigger: Importing an icon pack whose pack.json declares an icon with a relative filename containing ../ traversal or an absolute path, making destFile resolve outside the pack directory.

Common situations: Malicious icon packs attempting to overwrite arbitrary app files (covered by testMaliciousIconPackCannotOverwriteVaultFile); hand-edited pack.json with bad filename fields; Windows-authored archives with backslash paths that canonicalize unexpectedly.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of beemdevelopment/Aegis@d6f4e5925a (2026-09-08). Data as JSON: /api/errors/6bd6d19348ff49e7. Report an issue: GitHub.

Appendix: source

Thrown at app/src/main/java/com/beemdevelopment/aegis/icons/IconPackManager.java:99

            if (!packDir.getCanonicalPath().startsWith(_iconsBaseDir.getCanonicalPath() + File.separator)) {
                throw new IOException("Attempted to write outside of the parent directory");
            }
            if (packDir.exists()) {
                throw new IconPackExistsException(pack);
            }
            IconPack existingPack = getIconPackByUUID(pack.getUUID());
            if (existingPack != null) {
                throw new IconPackExistsException(existingPack);
            }
            if (!packDir.exists() && !packDir.mkdirs()) {
                throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
            }

            // extract each of the defined icons to the icon pack directory
            for (IconPack.Icon icon : pack.getIcons()) {
                File destFile = new File(packDir, icon.getRelativeFilename());
                if (!destFile.getCanonicalPath().startsWith(packDir.getCanonicalPath() + File.separator)) {
                    throw new IOException("Attempted to write outside of the icon pack directory");
                }
                FileHeader iconHeader = zipFile.getFileHeader(icon.getRelativeFilename());
                if (iconHeader == null) {
                    throw new IOException(String.format("Unable to find %s relative to the root of the ZIP file", icon.getRelativeFilename()));
                }

                // create new directories for this file if needed
                File parent = destFile.getParentFile();
                if (parent != null && !parent.exists() && !parent.mkdirs()) {
                    throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
                }

                try (ZipInputStream inStream = zipFile.getInputStream(iconHeader);
                     FileOutputStream outStream = new FileOutputStream(destFile)) {
                    IOUtils.copy(inStream, outStream);
                }

                // after successful copy of the icon, store the new filename

View on GitHub (pinned to d6f4e5925a)