{"record":{"id":"92c4e7ed413b033d","repo":"ppy/osu","slug":"filename-filenames-original-is-not-allowed","errorCode":null,"errorMessage":"Filename \"{filenames.original}\" is not allowed.","messagePattern":"Filename \"(.+?)\" is not allowed\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"osu.Game/Database/RealmArchiveModelImporter.cs","lineNumber":374,"sourceCode":"                    LogForModel(item, @\"Found existing (optimised) but failed pre-check.\");\r\n                }\r\n            }\r\n\r\n            try\r\n            {\r\n                // Log output here will be missing a valid hash in non-batch imports.\r\n                LogForModel(item, $@\"Beginning import from {archive?.Name ?? \"unknown\"}...\");\r\n\r\n                List<RealmNamedFileUsage> files = new List<RealmNamedFileUsage>();\r\n\r\n                if (archive != null)\r\n                {\r\n                    // Import files to the disk store.\r\n                    // We intentionally delay adding to realm to avoid blocking on a write during disk operations.\r\n                    foreach (var filenames in getShortenedFilenames(archive))\r\n                    {\r\n                        if (FilesystemSanityCheckHelpers.IncursPathTraversalRisk(filenames.shortened))\r\n                            throw new InvalidOperationException($@\"Filename \"\"{filenames.original}\"\" is not allowed.\");\r\n\r\n                        using (Stream s = archive.GetStream(filenames.original))\r\n                            files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false, parameters.PreferHardLinks), filenames.shortened));\r\n                    }\r\n                }\r\n\r\n                using (var transaction = realm.BeginWrite())\r\n                {\r\n                    // Add all files to realm in one go.\r\n                    // This is done ahead of the main transaction to ensure we can correctly cleanup the files, even if the import fails.\r\n                    foreach (var file in files)\r\n                    {\r\n                        if (!file.File.IsManaged)\r\n                            realm.Add(file.File, true);\r\n                    }\r\n\r\n                    transaction.Commit();\r\n                }\r","sourceCodeStart":356,"sourceCodeEnd":392,"githubUrl":"https://github.com/ppy/osu/blob/d9c73e12adff2feaae4a3e158d36fe5883faf6ca/osu.Game/Database/RealmArchiveModelImporter.cs#L356-L392","documentation":"Thrown by RealmArchiveModelImporter during archive import when getShortenedFilenames(archive) yields a shortened filename that FilesystemSanityCheckHelpers.IncursPathTraversalRisk flags. The exception message echoes the ORIGINAL filename, not the shortened one. This guards the import path against malicious or malformed archive entries that try to escape the files directory.","triggerScenarios":"Importing an archive (.osz/.osk/etc.) whose internal entry names contain '../', absolute paths, or other traversal patterns. The check runs on every entry produced by getShortenedFilenames before the entry's stream is opened and added to the store.","commonSituations":"Importing a user-supplied or downloaded archive with crafted entry names (Zip Slip); archives produced by tools that embed full paths or backslash separators; cross-platform archives where separators normalise into traversal.","solutions":["Reject or rewrite offending entry names before import: take Path.GetFileName(entry) and drop any directory traversal.","Validate archives at the source; if from untrusted upload, run entries through IncursPathTraversalRisk and quarantine failures.","If a legitimately-nested archive structure is intended, confirm getShortenedFilenames' shortening rules and that relative subpaths are preserved without traversal."],"exampleFix":"// before\nforeach (var filenames in getShortenedFilenames(archive))\n{\n    if (FilesystemSanityCheckHelpers.IncursPathTraversalRisk(filenames.shortened))\n        throw new InvalidOperationException($\"Filename \\\"{filenames.original}\\\" is not allowed.\");\n    // ...\n}\n\n// after (skip & report instead of aborting whole import)\nforeach (var filenames in getShortenedFilenames(archive))\n{\n    if (FilesystemSanityCheckHelpers.IncursPathTraversalRisk(filenames.shortened))\n    {\n        LogForModel(item, $\"Skipped unsafe entry: {filenames.original}\");\n        continue;\n    }\n    // ...\n}","handlingStrategy":"validation","validationCode":"// Pre-scan archive entries before import and quarantine unsafe ones.\nforeach (var entry in archive filenames)\n{\n    if (FilesystemSanityCheckHelpers.IncursPathTraversalRisk(entry.shortened))\n        unsafeEntries.Add(entry.original);\n}\nif (unsafeEntries.Any()) throw new InvalidDataException($\"Unsafe entries: {string.Join(\", \", unsafeEntries)}\");","typeGuard":"static bool IsSafeArchiveEntry(string entry)\n    => !string.IsNullOrWhiteSpace(entry)\n       && !Path.IsPathRooted(entry)\n       && !entry.Contains(\"..\")\n       && !FilesystemSanityCheckHelpers.IncursPathTraversalRisk(entry);","tryCatchPattern":"try { importer.Import(archive); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"is not allowed\"))\n{ /* report malicious/corrupt archive to user, abort import */ }","preventionTips":["Treat all archive entry names as untrusted; never preserve absolute or traversal paths.","For user-uploaded archives, run a Zip-Slip pre-scan and reject before writing.","Normalise entry separators and strip leading slashes/Drive prefixes at the boundary."],"tags":["security","path-traversal","import","archive","zip-slip"],"backgroundTag":null,"analyzedSha":"d9c73e12adff2feaae4a3e158d36fe5883faf6ca","analyzedAt":"2026-08-13T14:12:54.015Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}