NationalSecurityAgency/ghidra · error · IOException

Bad filename in archive: \"" + filename + "\"

Error message

Bad filename in archive: \"" + filename + "\"

What it means

Thrown by RestoreTask while extracting a Ghidra Project Archive (.gar, a zip). mapSourceFilenameToDest() runs each archive entry name through FSUtilities.getSafeFilename(); if sanitization changes the name, the entry is rejected as unsafe and an IOException is thrown. This is a security guard against path traversal / zip-slip and against filesystem-illegal characters. getSafeFilename replaces / \ : | with '_', maps empty/"."/".." to fixed names, and percent-encodes control chars, non-ASCII, and '%','?','|'.

Source

Thrown at Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/archive/RestoreTask.java:146

			throws IOException, CancelledException {
		if (!shouldSkip(file)) {
			super.processFile(file, destFSFile, monitor);
		}
	}

	@Override
	protected void processDirectory(GFile srcGFileDirectory, File destDirectory,
			TaskMonitor monitor) throws IOException, CancelledException {
		if (!shouldSkip(srcGFileDirectory)) {
			super.processDirectory(srcGFileDirectory, destDirectory, monitor);
		}
	}

	@Override
	protected String mapSourceFilenameToDest(GFile srcFile) throws IOException {
		String filename = srcFile.getName();
		if (!FSUtilities.getSafeFilename(filename).equals(filename)) {
			throw new IOException("Bad filename in archive: \"" + filename + "\"");
		}
		return filename;
	}

	private boolean shouldSkip(GFile file) {
		String path = file.getPath().toLowerCase();
		if (FILES_TO_SKIP.contains(path)) {
			return true;
		}
		if (ArchivePlugin.OLD_FOLDER_PROPERTIES_FILE.equalsIgnoreCase(file.getName())) {
			// ignore this file in any directory in the archive
			return true;
		}
		String ext = "." + FilenameUtils.getExtension(file.getName());
		if (GhidraURL.MARKER_FILE_EXTENSION.equalsIgnoreCase(ext)) {
			// ignore .gpr marker files, any file name
			return true;
		}

View on GitHub (pinned to d5f144c24d)

Solutions

  1. Re-export the project from the original Ghidra installation (Archive > Save Project to Archive) so all entry names are clean.
  2. Inspect the archive contents: run `unzip -l file.gar` (or `jar tf`) and look for any entry containing slashes, colons, or other unsafe characters; the bad filename is shown verbatim in the error message.
  3. If only a few benign entries are flagged (e.g. cross-platform name differences), rebuild the zip without those characters and re-add the .gar marker file that verifyArchive() requires.
  4. If a freshly Ghidra-exported archive fails, report it as a bug with the offending entry name rather than disabling the check.

Example fix

// The check is internal; fix the archive contents instead.
// Before (archive entry with an unsafe name):
//   project/prj:backup   <- ':' triggers the guard
// After (rename the entry, then re-zip):
//   project/prj_backup
//
// Programmatic pre-scan to find offending entries before restoring:
try (GFileSystem fs = fsService.openFileSystemContainer(fsrl, monitor)) {
    for (GFile f : fs.lookup("/").getListing()) {
        String n = f.getName();
        if (!FSUtilities.getSafeFilename(n).equals(n)) {
            System.err.println("Unsafe entry: " + n);
        }
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Scan the archive's entry names BEFORE invoking RestoreTask,
// flagging any name the sanitizer would change.
try (GFileSystem fs = fsService.openFileSystemContainer(archiveFSRL, monitor)) {
    java.util.List<String> bad = new java.util.ArrayList<>();
    fs.walkFileTree("/", (f, __) -> {
        String n = f.getName();
        if (n != null && !ghidra.formats.gfilesystem.FSUtilities
                .getSafeFilename(n).equals(n)) {
            bad.add(n);
        }
    });
    if (!bad.isEmpty()) {
        // refuse to restore; report bad names to the user
        throw new IOException("Refusing archive with unsafe names: " + bad);
    }
}

Try / catch

// RestoreTask.run() already catches Throwable and surfaces a dialog.
// For programmatic restoration, wrap the extraction in try/catch and
// report the offending filename from the IOException message.
try {
    restoreTask.run(monitor);
} catch (IOException e) {
    if (e.getMessage().startsWith("Bad filename in archive:")) {
        // log/report e.getMessage(); do not retry the same archive unchanged
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: Restoring a .gar whose zip entries contain any of '/' '\\' ':' '|', the chars '%','?','|', control characters (codepoint <32), non-ASCII (>126), or whose name is empty, ".", or "..". Triggered during the startExtract()/processFile() traversal when mapSourceFilenameToDest() is called for each entry.

Common situations: Archive produced or hand-edited on another OS, a corrupted/tampered .gar, an archive that survived a failed transfer or unzip/re-zip round trip, or a maliciously crafted archive attempting zip-slip path traversal (e.g. an entry named "../../etc/x"). Rare for archives exported by a matching Ghidra version.

Related errors


AI-assisted analysis of NationalSecurityAgency/ghidra@d5f144c24d (2026-08-14). Data as JSON: /api/errors/330a6b2e2eff3a2a. Report an issue: GitHub.