mirror of
https://github.com/anchore/syft.git
synced 2026-11-03 16:06:36 +01:00
fix: escape % in ArchivePath segments
`:` was escaped as `%3A`, but `%` itself wasn't, so an entry named `a:b.jar` and one named `a%3Ab.jar` got the same `ArchivePath`. Their contents then collide in `FileMetadata` / `FileDigests` (last writer wins) and share artifact IDs. `%` is now escaped as `%25` before `:`, which makes the encoding injective. The encoding is documented on `Coordinates.ArchivePath` and in the schema. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
@@ -21,16 +21,21 @@ type Traversal struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// VirtualPath returns the colon-delimited chain of archives from the scan root to loc, ending in loc
|
// VirtualPath returns the colon-delimited chain of archives from the scan root to loc, ending in loc
|
||||||
// itself: "app.war:WEB-INF/lib/dep.jar". Outside any archive it is the location's path unchanged. A
|
// itself: "app.war:WEB-INF/lib/dep.jar". Outside any archive it is the location's path unchanged. In an
|
||||||
// colon in an entry name is escaped so splitting on ':' recovers the archive boundaries; the leading
|
// entry name '%' is escaped as "%25" and then ':' as "%3A", so splitting on ':' recovers the archive
|
||||||
// slash is dropped to match the java cataloger, which joins slash-less zip entry names onto the
|
// boundaries and no two entry names share an escaped form. The leading slash is dropped to match the
|
||||||
// containing archive's path.
|
// java cataloger, which joins slash-less zip entry names onto the containing archive's path.
|
||||||
func VirtualPath(loc file.Location) string {
|
func VirtualPath(loc file.Location) string {
|
||||||
if loc.ArchivePath == "" {
|
if loc.ArchivePath == "" {
|
||||||
return loc.Path()
|
return loc.Path()
|
||||||
}
|
}
|
||||||
entryPath := strings.ReplaceAll(loc.Path(), ":", "%3A")
|
return loc.ArchivePath + ":" + strings.TrimPrefix(escapeEntryPath(loc.Path()), "/")
|
||||||
return loc.ArchivePath + ":" + strings.TrimPrefix(entryPath, "/")
|
}
|
||||||
|
|
||||||
|
var entryPathEscaper = strings.NewReplacer("%", "%25", ":", "%3A")
|
||||||
|
|
||||||
|
func escapeEntryPath(p string) string {
|
||||||
|
return entryPathEscaper.Replace(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
type traversalCtxKey struct{}
|
type traversalCtxKey struct{}
|
||||||
|
|||||||
@@ -85,14 +85,14 @@ func TestVirtualPath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVirtualPath_theEscapeCharacterIsNotItselfEscaped(t *testing.T) {
|
func TestVirtualPath_theEscapeCharacterIsItselfEscaped(t *testing.T) {
|
||||||
// an accepted limit: an entry path holding the literal text %3A is indistinguishable from one holding
|
// an entry path holding the literal text %3A must not collide with one holding a colon, or their
|
||||||
// a colon. Escaping % too would move many more real identities than it protects.
|
// contents share an ArchivePath and overwrite each other's file metadata
|
||||||
fromColon := VirtualPath(locationIn("outer.zip", "a:b.jar"))
|
fromColon := VirtualPath(locationIn("outer.zip", "a:b.jar"))
|
||||||
fromLiteral := VirtualPath(locationIn("outer.zip", "a%3Ab.jar"))
|
fromLiteral := VirtualPath(locationIn("outer.zip", "a%3Ab.jar"))
|
||||||
|
|
||||||
assert.Equal(t, "outer.zip:a%3Ab.jar", fromColon)
|
assert.Equal(t, "outer.zip:a%3Ab.jar", fromColon)
|
||||||
assert.Equal(t, fromColon, fromLiteral)
|
assert.Equal(t, "outer.zip:a%253Ab.jar", fromLiteral)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVirtualPath_escapingComposesAcrossNestingLevels(t *testing.T) {
|
func TestVirtualPath_escapingComposesAcrossNestingLevels(t *testing.T) {
|
||||||
|
|||||||
@@ -724,7 +724,7 @@
|
|||||||
},
|
},
|
||||||
"archivePath": {
|
"archivePath": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "ArchivePath is the colon-delimited chain of archive paths, from the scan root, traversed to reach this file (e.g. \"app.war:WEB-INF/lib/dep.jar\"). Blank for files not found within an archive. It disambiguates identically-named files in different archives on the same filesystem."
|
"description": "ArchivePath is the colon-delimited chain of archive paths, from the scan root, traversed to reach this file (e.g. \"app.war:WEB-INF/lib/dep.jar\"). Blank for files not found within an archive. It disambiguates identically-named files in different archives on the same filesystem. Within each segment after the first, '%' is escaped as \"%25\" and ':' as \"%3A\", so splitting on ':' recovers the archive boundaries."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -2490,7 +2490,7 @@
|
|||||||
},
|
},
|
||||||
"archivePath": {
|
"archivePath": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "ArchivePath is the colon-delimited chain of archive paths, from the scan root, traversed to reach this file (e.g. \"app.war:WEB-INF/lib/dep.jar\"). Blank for files not found within an archive. It disambiguates identically-named files in different archives on the same filesystem."
|
"description": "ArchivePath is the colon-delimited chain of archive paths, from the scan root, traversed to reach this file (e.g. \"app.war:WEB-INF/lib/dep.jar\"). Blank for files not found within an archive. It disambiguates identically-named files in different archives on the same filesystem. Within each segment after the first, '%' is escaped as \"%25\" and ':' as \"%3A\", so splitting on ':' recovers the archive boundaries."
|
||||||
},
|
},
|
||||||
"accessPath": {
|
"accessPath": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -724,7 +724,7 @@
|
|||||||
},
|
},
|
||||||
"archivePath": {
|
"archivePath": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "ArchivePath is the colon-delimited chain of archive paths, from the scan root, traversed to reach this file (e.g. \"app.war:WEB-INF/lib/dep.jar\"). Blank for files not found within an archive. It disambiguates identically-named files in different archives on the same filesystem."
|
"description": "ArchivePath is the colon-delimited chain of archive paths, from the scan root, traversed to reach this file (e.g. \"app.war:WEB-INF/lib/dep.jar\"). Blank for files not found within an archive. It disambiguates identically-named files in different archives on the same filesystem. Within each segment after the first, '%' is escaped as \"%25\" and ':' as \"%3A\", so splitting on ':' recovers the archive boundaries."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -2490,7 +2490,7 @@
|
|||||||
},
|
},
|
||||||
"archivePath": {
|
"archivePath": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "ArchivePath is the colon-delimited chain of archive paths, from the scan root, traversed to reach this file (e.g. \"app.war:WEB-INF/lib/dep.jar\"). Blank for files not found within an archive. It disambiguates identically-named files in different archives on the same filesystem."
|
"description": "ArchivePath is the colon-delimited chain of archive paths, from the scan root, traversed to reach this file (e.g. \"app.war:WEB-INF/lib/dep.jar\"). Blank for files not found within an archive. It disambiguates identically-named files in different archives on the same filesystem. Within each segment after the first, '%' is escaped as \"%25\" and ':' as \"%3A\", so splitting on ':' recovers the archive boundaries."
|
||||||
},
|
},
|
||||||
"accessPath": {
|
"accessPath": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type Coordinates struct {
|
|||||||
// FileSystemID is an ID representing an entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank. A file inside an archive carries the FileSystemID of the filesystem the archive was found in.
|
// FileSystemID is an ID representing an entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank. A file inside an archive carries the FileSystemID of the filesystem the archive was found in.
|
||||||
FileSystemID string `json:"layerID,omitempty" cyclonedx:"layerID"`
|
FileSystemID string `json:"layerID,omitempty" cyclonedx:"layerID"`
|
||||||
|
|
||||||
// ArchivePath is the colon-delimited chain of archive paths, from the scan root, traversed to reach this file (e.g. "app.war:WEB-INF/lib/dep.jar"). Blank for files not found within an archive. It disambiguates identically-named files in different archives on the same filesystem.
|
// ArchivePath is the colon-delimited chain of archive paths, from the scan root, traversed to reach this file (e.g. "app.war:WEB-INF/lib/dep.jar"). Blank for files not found within an archive. It disambiguates identically-named files in different archives on the same filesystem. Within each segment after the first, '%' is escaped as "%25" and ':' as "%3A", so splitting on ':' recovers the archive boundaries.
|
||||||
ArchivePath string `json:"archivePath,omitempty" cyclonedx:"archivePath"`
|
ArchivePath string `json:"archivePath,omitempty" cyclonedx:"archivePath"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user