{"record":{"id":"e169f567eec657d8","repo":"spring-projects/spring-ai","slug":"invalid-filename-for-file-e169f5","errorCode":null,"errorMessage":"Invalid filename for file '': ''","messagePattern":"Invalid filename for file '': ''","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicSkillsResponseHelper.java","lineNumber":169,"sourceCode":"\t\t\tthrow new IOException(\"Invalid filename for file '\" + fileId + \"': null or blank\");\n\t\t}\n\t\tPath name;\n\t\ttry {\n\t\t\tname = Path.of(rawName);\n\t\t}\n\t\tcatch (InvalidPathException ex) {\n\t\t\tthrow new IOException(\"Invalid filename for file '\" + fileId + \"': \" + rawName, ex);\n\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()) {","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicSkillsResponseHelper.java#L151-L187","documentation":"resolveSafeChildPath rejects filenames that resolve to '.' or '..' (or otherwise normalize to the target directory itself), throwing this IOException with the raw name. It prevents a download from overwriting the target directory or escaping it via relative-name tricks. The single-segment rule has already passed; this is the dot-segment guard.","triggerScenarios":"The API-reported filename is '.', '..', './' or '.\\' (e.g. empty-ish or self-referential names after normalization), so the extracted single segment equals '.' or '..'.","commonSituations":"Directory entries included in a skills listing and passed through as files; truncated or empty name fields that normalize to '.'; unusual zip/skill archives that contain directory markers.","solutions":["Filter out directory entries ('.'/'..') from the API file list before calling filePath.","Validate the filename client-side: skip files whose name is null, blank, or equals '.'/'..'.","Sanitize by generating a fallback name (e.g. file-<fileId>.bin) when the reported name is not a real filename.","Catch the IOException and log/skip the offending entry."],"exampleFix":"// before\nhelper.filePath(fileId, name);\n// after\nif (name == null || name.isBlank() || name.equals(\".\") || name.equals(\"..\")) {\n    return; // skip non-file entry\n}\nhelper.filePath(fileId, name);","handlingStrategy":"validation","validationCode":"static boolean isRealFileName(String name) {\n    return name != null && !name.isBlank()\n        && !name.equals(\".\") && !name.equals(\"..\");\n}","typeGuard":"static String requireFileName(String name) {\n    if (!isRealFileName(name)) throw new IllegalArgumentException(\"Not a filename: \" + name);\n    return name;\n}","tryCatchPattern":"try {\n    Path p = helper.filePath(fileId, name);\n} catch (IOException e) {\n    log.warn(\"Skipping non-file entry {}\", name, e);\n}","preventionTips":["Filter directory entries out of file listings before download.","Sanitize or regenerate fallback names for blank/self-referential names.","Treat API filenames as untrusted data, never as trusted paths."],"tags":["path-validation","io","filename"],"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-14T05:17:10.506Z"}