fix: validate post-transformation path shapes and assert containment at resolve sinks - #2079
fix: validate post-transformation path shapes and assert containment at resolve sinks#2079gnodet wants to merge 2 commits into
Conversation
08b6710 to
4596344
Compare
… path traversal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4596344 to
702cfc9
Compare
| } | ||
| } | ||
| Path file = basePath.resolve(relative).normalize(); | ||
| if (!file.startsWith(basePath.normalize())) { |
There was a problem hiding this comment.
This is overly aggressive (to normalize unchanging basepath for each file). IMHO it is already normalized in ctor, should be just used, no?
There was a problem hiding this comment.
Good catch — moved normalize() into the ctor (this.basePath = requireNonNull(basePath).normalize()). The containment check now uses basePath directly. Also fixed @since 2.0.22 → 2.0.23 across the PR.
| */ | ||
| private static String resolveContained(Path basedir, String name) { | ||
| Path resolved = basedir.resolve(name).toAbsolutePath(); | ||
| if (!resolved.normalize().startsWith(basedir.toAbsolutePath().normalize())) { |
There was a problem hiding this comment.
Again, doing this for each lock... basedir should be treated like this in ctor?
There was a problem hiding this comment.
Same fix applied here: basePath is now normalized at construction time (path.toAbsolutePath().normalize()), and the session-resolved fallback path is also normalized. resolveContained() now just does resolve().normalize() + startsWith(basedir) without redundant per-call normalization.
gnodet
left a comment
There was a problem hiding this comment.
Thorough, well-layered security fix addressing 4 audit findings (f001, f003, f010, f017) about path traversal and containment. Defense-in-depth is correctly applied at input validation, sanitization, post-composition, and resolve-sink layers.
@since tag corrections
Three new methods/constants have @since 2.0.22 but should be @since 2.0.23 (2.0.22 is already released):
PathUtils.validateDotSeparatedPathComponent— public API, will produce incorrect availability docsGAVNameMapper.fieldToSegment— protected, part of API surfaceBasedirNameMapper.resolveContained— private, lower impact
Minor observation (not blocking)
SummaryFileTrustedChecksumsSource.summaryFile() splices the repository key into a filename via string concatenation then passes to Path.resolve(). The PR adds validatePathComponent in SparseDirectoryTrustedChecksumsSource for the same pattern but not here. Risk is low (default key function sanitizes via stringToPathSegment()), but a custom RepositoryKeyFunction returning / could escape. Consider adding the same validation for consistency.
Positive observations
validateDotSeparatedPathComponentcorrectly identifies that groupId dot-to-slash expansion can produce absolute paths (.home.ci→/home/ci) and authority URIs (..evilhost→//evilhost)- Test coverage is comprehensive across all modified sinks
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of gnodet
Address review feedback from cstamas: - FileTransporter: normalize basePath once in ctor instead of per-file - BasedirNameMapper: normalize basedir in ctor and session-resolved path - Fix @SInCE 2.0.22 → 2.0.23 (2.0.22 is already released) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Closing — all path-traversal fixes from this PR have been superseded by cstamas's independent implementations:
All findings (f001, f003, f010, f017) from the security audit are now addressed in master. |
Summary
Fixes 4 findings from the maven-resolver security audit (scan-maven-resolver-20260811):
validatePathComponent: absolute-path writes outside local repo and cross-host fetchesFileTransportertraversal guard misses absolute paths and Windows separators".."escapes origin-aware trusted-checksums and split-LRM directoriesRoot cause: Strings from remote repositories (artifact coordinates, repository ids) are validated in their pre-transformation shape and then passed to
Path.resolve(), which returns absolute arguments unchanged. No sink asserts containment.Fix: Validate post-replacement path shape, reject
.././:segments, and assertnormalize().startsWith(base)containment at every resolve sink.Test plan
Generated with Claude Code