{"record":{"id":"2b9143cb7edf1a1b","repo":"spring-projects/spring-ai","slug":"invalid-filename-for-file-resolves-outside-tar","errorCode":null,"errorMessage":"Invalid filename for file '': resolves outside target directory ''","messagePattern":"Invalid filename for file '': resolves outside target directory ''","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicSkillsResponseHelper.java","lineNumber":177,"sourceCode":"\t\t}\n\t\tif (name.isAbsolute() || name.getRoot() != null) {\n\t\t\tthrow new IOException(\"Invalid filename for file '\" + fileId + \"': absolute path '\" + rawName + \"'\");\n\t\t}\n\t\tif (name.getNameCount() != 1) {\n\t\t\tthrow new IOException(\n\t\t\t\t\t\"Invalid filename for file '\" + fileId + \"': must be a single path segment '\" + rawName + \"'\");\n\t\t}\n\t\tString only = name.getName(0).toString();\n\t\tif (only.equals(\".\") || only.equals(\"..\")) {\n\t\t\tthrow new IOException(\"Invalid filename for file '\" + fileId + \"': '\" + rawName + \"'\");\n\t\t}\n\n\t\t// One extra hardening check to make sure nothing fell through the cracks above\n\t\t// (future tweaks to the rules, odd platform path quirks, etc.).\n\t\tPath base = targetDir.toAbsolutePath().normalize();\n\t\tPath resolved = base.resolve(only).normalize();\n\t\tif (!resolved.startsWith(base)) {\n\t\t\tthrow new IOException(\n\t\t\t\t\t\"Invalid filename for file '\" + fileId + \"': resolves outside target directory '\" + rawName + \"'\");\n\t\t}\n\t\treturn resolved;\n\t}\n\n\tprivate static void extractFileIdsFromBashResult(BashCodeExecutionToolResultBlock resultBlock,\n\t\t\tList<String> fileIds) {\n\t\tBashCodeExecutionToolResultBlock.Content content = resultBlock.content();\n\t\tif (content.isBashCodeExecutionResultBlock()) {\n\t\t\tfor (BashCodeExecutionOutputBlock outputBlock : content.asBashCodeExecutionResultBlock().content()) {\n\t\t\t\tfileIds.add(outputBlock.fileId());\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate static void extractFileIdsFromCodeExecutionResult(CodeExecutionToolResultBlock resultBlock,\n\t\t\tList<String> fileIds) {\n\t\tCodeExecutionToolResultBlockContent content = resultBlock.content();","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicSkillsResponseHelper.java#L159-L195","documentation":"Final hardening check in resolveSafeChildPath: after resolving the single-segment name against the normalized absolute target directory, it throws this IOException if the resolved path does not remain under the base directory. This backstops any future rule changes or platform path quirks that could let a file land outside the target directory (a path-traversal attack).","triggerScenarios":"A filename that, after normalize(), escapes the base dir — e.g. via platform-specific quirks or a name that survived earlier checks such as a case where resolve introduces '..' behavior (symbolic-link-adjacent or driver/root-relative oddities).","commonSituations":"Hostile filenames returned by a compromised or mocked API endpoint; unusual filesystems (Windows drive-relative paths); regression after changing validation rules.","solutions":["Treat the filename as untrusted: reject any name containing separators, '..', or non-portable characters before calling filePath.","Verify your target directory is not itself a symlink whose parent chain changes resolution semantics.","Audit where the filename originates (API response field vs. user input) and whitelist alphanumerics/dots/dashes/hyphens.","Catch the IOException, log the rejected name, and abort processing that file."],"exampleFix":"// before\nPath p = helper.filePath(fileId, apiReturnedName);\n// after\nif (!apiReturnedName.matches(\"[A-Za-z0-9._-]+\")) {\n    throw new IllegalArgumentException(\"Unsafe filename: \" + apiReturnedName);\n}\nPath p = helper.filePath(fileId, apiReturnedName);","handlingStrategy":"validation","validationCode":"static boolean staysUnderBase(Path base, String name) {\n    return base.toAbsolutePath().normalize()\n        .resolve(name).normalize().startsWith(base.toAbsolutePath().normalize());\n}","typeGuard":"static String strictSafeName(String name) {\n    if (name == null || name.contains(\"/\") || name.contains(\"\\\\\")\n        || !name.matches(\"[A-Za-z0-9._-]+\")) {\n        throw new IllegalArgumentException(\"Unsafe filename: \" + name);\n    }\n    return name;\n}","tryCatchPattern":"try {\n    Path p = helper.filePath(fileId, name);\n} catch (IOException e) {\n    securityLog.warn(\"Path traversal attempt rejected: {}\", name);\n    throw e;\n}","preventionTips":["Reject any filename containing path separators or '..' at your API boundary.","Keep the defense-in-depth check enabled; never bypass it in production.","Audit sources of filenames (API responses, user uploads) and whitelist formats."],"tags":["path-validation","security","path-traversal"],"backgroundTag":"path-traversal-blocked","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}