CodeQL 2.26.1 improved Java path-injection analysis by recognizing suitable jakarta.validation.constraints.Pattern and javax.validation.constraints.Pattern annotations as sanitizers. This can reduce false positives where an application already limits a path component to a safe character set.
The change does not mean every @Pattern makes filesystem access safe. The regular expression must actually prevent separators, traversal tokens, absolute paths, and ambiguous encoded forms.
Quick answer
Use a strict allowlist for one path component, resolve it beneath a server-owned root, normalize the result, and verify that the normalized path remains inside that root.
public record ExportRequest(
@Pattern(regexp = "[A-Za-z0-9_-]{1,64}")
String reportId
) {}
Path resolveReport(String reportId) {
Path root = Path.of("/srv/reports").toAbsolutePath().normalize();
Path candidate = root.resolve(reportId + ".pdf").normalize();
if (!candidate.startsWith(root)) {
throw new IllegalArgumentException("Invalid report path");
}
return candidate;
}
The regex restricts one identifier. It does not attempt to declare an entire user-provided path safe.
What changed in CodeQL
Static analysis follows untrusted data from a source to a filesystem sink. Previously, CodeQL could continue that flow through a Bean Validation annotation even when the annotation’s allowlist made traversal impossible. The new modeling can recognize safe patterns and remove that noise.
Fewer false positives matter because reviewers can focus on reachable weaknesses. However, a disappearing alert is not proof that every runtime validation path is active. Bean Validation must actually run for the object before the file operation.
Patterns that remain risky
A broad expression can still allow path control:
@Pattern(regexp = ".+")
String file;
So can expressions that permit /, \, colons, percent signs, or leading dots without a clear reason. Negative checks such as “does not contain ../” miss alternate separators, repeated encoding, absolute paths, and platform-specific behavior.
Prefer a small positive alphabet and a tight length bound. If the user is choosing an existing file, consider using an opaque database ID and look up the server-owned path instead.
Confirm validation really executes
In Spring, annotations generally need @Valid or @Validated at the appropriate boundary. Direct object construction, deserialization in a background consumer, or an internal method call may bypass the expected controller validation.
Write a test that sends an invalid value through the same entry point production uses:
mockMvc.perform(post("/exports")
.contentType("application/json")
.content("""{"reportId":"../../etc/passwd"}"""))
.andExpect(status().isBadRequest());
Then assert that the repository or filesystem adapter was not called.
Review alerts after the upgrade
Compare CodeQL results before and after updating the analysis bundle. For any result that disappears, inspect the annotation and the route that triggers validation. Keep the test even when CodeQL accepts the sanitizer because the runtime contract can regress independently.
For an alert that remains, look for:
- path input assembled across multiple variables;
- validation annotations not executed on that code path;
- a regex that permits separators or traversal;
- normalization without a containment check;
- symbolic links crossing the expected root;
- archive extraction that trusts entry names.
Do not add an annotation only to satisfy a scanner. Define the product-level identifier first, then encode that rule in validation.
A practical decision rule
If users need to select a report, avatar, or export, accept its ID rather than its storage path. If users genuinely need nested paths, parse each segment, reject empty and dot segments, and apply authorization after resolution. A valid filename is not necessarily a file that this user may access.
Keep authorization separate from path safety
A path can be syntactically safe and still identify another user’s file. After resolving a validated identifier, check ownership or role authorization using the canonical record. Do not infer access from a directory name supplied by the client.
For downloads, open the file only after both checks pass and return a generic not-found response when revealing existence would leak information. Log the canonical resource ID rather than a raw rejected path. This keeps the security trail useful without preserving attacker-controlled traversal strings.
If symbolic links are possible beneath the storage root, decide whether they are allowed and test that policy explicitly. A normalized lexical path can remain under the root while a link resolves elsewhere.
Primary sources
- CodeQL 2.26.1 analysis update
- OWASP Path Traversal
- Jakarta Bean Validation specification
- CodeQL Java path injection query help
The improved sanitizer model is useful, but the durable fix remains architectural: accept an identifier, construct the path on the server, normalize it, enforce containment, and test the real runtime validation path.
Discussion
What would you try, change, or challenge after reading this guide? Specific results and errors help the next reader.
Comments will load as you reach this section.