{"record":{"id":"a8352b05cf39865b","repo":"spring-projects/spring-ai","slug":"invalid-filename-for-file-absolute-path","errorCode":null,"errorMessage":"Invalid filename for file '': absolute path ''","messagePattern":"Invalid filename for file '': absolute path ''","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicSkillsResponseHelper.java","lineNumber":161,"sourceCode":"\t * Validate an API-provided filename and resolve it to a child of {@code targetDir}.\n\t * Rejects null/blank names, absolute paths, names containing path separators or\n\t * {@code .}/{@code ..} segments, and names that resolve outside {@code targetDir}.\n\t * Filenames come from model-influenced API metadata and must not be trusted as safe\n\t * path components.\n\t */\n\tstatic Path resolveSafeChildPath(Path targetDir, @Nullable String rawName, String fileId) throws IOException {\n\t\tif (rawName == null || rawName.isBlank()) {\n\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}","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicSkillsResponseHelper.java#L143-L179","documentation":"resolveSafeChildPath rejects filenames that are absolute paths or carry a filesystem root. Since names originate from model-influenced API metadata, allowing '/etc/passwd' or 'C:\\evil' would enable writing outside the target directory. The check name.isAbsolute() || name.getRoot() != null throws an IOException naming the offending raw value.","triggerScenarios":"A file entry whose name field contains an absolute path like '/tmp/x.txt', '\\\\server\\share\\f' (UNC), or a Windows rooted path 'C:\\file.txt' passed to resolveSafeChildPath.","commonSituations":"A compromised or hallucinating model returning full paths instead of bare filenames; proxying responses from another system that emits absolute paths; hostile API payloads probing for path traversal (this check is part of the traversal defense).","solutions":["Strip any directory portion and keep only the base filename (e.g. Path.of(rawName).getFileName()) before resolving, after verifying it is a single safe segment.","Reject/skip the file entry and log it — absolute paths from this API are never legitimate.","Treat the occurrence as a data-quality or security signal: inspect the upstream response source for prompt injection.","Keep using resolveSafeChildPath for every API-derived filename; do not bypass it with direct Path.of/Paths.get(targetDir, rawName)."],"exampleFix":"// before\nPath p = resolveSafeChildPath(targetDir, \"/etc/passwd\", fileId); // throws\n// after\nString bare = Path.of(rawName).getFileName().toString();\nif (!bare.equals(rawName)) {\n    log.warn(\"Rejected absolute path from API for file {}\", fileId);\n    return null;\n}\nPath p = resolveSafeChildPath(targetDir, bare, fileId);","handlingStrategy":"validation","validationCode":"Path n = Path.of(rawName);\nif (n.isAbsolute() || n.getRoot() != null || n.getNameCount() != 1) {\n    throw new SecurityException(\"Refusing non-simple filename from API: \" + rawName);\n}","typeGuard":"static boolean isSimpleRelativeName(String rawName) {\n    try {\n        Path n = Path.of(rawName);\n        return !n.isAbsolute() && n.getRoot() == null && n.getNameCount() == 1;\n    } catch (InvalidPathException e) { return false; }\n}","tryCatchPattern":"try {\n    Path p = AnthropicSkillsResponseHelper.resolveSafeChildPath(targetDir, rawName, fileId);\n} catch (IOException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"absolute path\")) {\n        securityLog.warn(\"Path-traversal attempt in file {}: {}\", fileId, rawName);\n    } else throw e;\n}","preventionTips":["Never bypass resolveSafeChildPath with direct targetDir.resolve(rawName).","Log and alert on absolute-path filenames — they can indicate prompt injection.","Keep the helper's validation as the single choke point for API-derived filenames."],"tags":["java","spring-ai","anthropic","skills","path-traversal","security"],"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"}