@@ -10,6 +10,7 @@ import (
1010 "io"
1111 "os"
1212 "path/filepath"
13+ "regexp"
1314 "strings"
1415 "time"
1516
@@ -18,6 +19,23 @@ import (
1819 "github.com/google/go-containerregistry/pkg/v1/tarball"
1920)
2021
22+ // ErrInvalidDigest indicates that an identifier shaped like a digest
23+ // (e.g. "sha256:...") does not match the expected format. Rejecting these
24+ // prevents path-traversal when the digest is used as a filename component.
25+ var ErrInvalidDigest = errors .New ("invalid artifact digest" )
26+
27+ // sha256DigestRe matches a well-formed sha256 digest: 64 lowercase hex chars.
28+ var sha256DigestRe = regexp .MustCompile (`^sha256:[0-9a-f]{64}$` )
29+
30+ // validateDigest returns nil if digest is a well-formed sha256 digest,
31+ // or a wrapped ErrInvalidDigest otherwise.
32+ func validateDigest (digest string ) error {
33+ if ! sha256DigestRe .MatchString (digest ) {
34+ return fmt .Errorf ("%w: %q" , ErrInvalidDigest , digest )
35+ }
36+ return nil
37+ }
38+
2139// ErrStoreCorrupted indicates that the local artifact store is in an
2240// inconsistent or partially missing state (e.g. missing tar, refs or metadata).
2341// Callers may safely attempt to re-fetch the artifact from the remote source.
@@ -79,6 +97,11 @@ func (s *Store) StoreArtifact(img v1.Image, reference string) (string, error) {
7997
8098 digestStr := digest .String ()
8199
100+ // Validate the digest before using it in filesystem paths (defense-in-depth).
101+ if err := validateDigest (digestStr ); err != nil {
102+ return "" , err
103+ }
104+
82105 tarPath := filepath .Join (s .baseDir , digestStr + ".tar" )
83106
84107 if err := crane .Save (img , reference , tarPath ); err != nil {
@@ -266,27 +289,32 @@ func (s *Store) DeleteArtifact(identifier string) error {
266289
267290// resolveIdentifier resolves a user-provided identifier (digest or reference)
268291// into a concrete content digest stored in the local artifact store.
292+ //
293+ // The returned digest is always strictly validated ("sha256:" + 64 hex chars)
294+ // so it can be safely used as a filename component without enabling path
295+ // traversal.
269296func (s * Store ) resolveIdentifier (identifier string ) (string , error ) {
270- // If the identifier is already a bare digest, return it directly .
297+ // Bare digest, e.g. "sha256:abc123..." .
271298 if strings .HasPrefix (identifier , "sha256:" ) {
299+ if err := validateDigest (identifier ); err != nil {
300+ return "" , err
301+ }
272302 return identifier , nil
273303 }
274304
275- // If the identifier is a digest reference (e.g. "repo@sha256:abc..."),
276- // extract and return the digest portion directly. Digest references
277- // are content-addressable, so the digest alone identifies the artifact.
305+ // Digest reference, e.g. "repo@sha256:abc123...".
278306 if i := strings .LastIndex (identifier , "@sha256:" ); i >= 0 {
279- return identifier [i + 1 :], nil
307+ digest := identifier [i + 1 :]
308+ if err := validateDigest (digest ); err != nil {
309+ return "" , err
310+ }
311+ return digest , nil
280312 }
281313
282- // If no tag is provided, default to ":latest".
283- // This mirrors standard OCI reference semantics.
314+ // Tagged or tag-less reference. Default to ":latest" per OCI semantics.
284315 if ! strings .Contains (identifier , ":" ) {
285316 identifier += ":latest"
286317 }
287-
288- // Resolve the reference to a digest via the refs store.
289- // Any failure here indicates the local store is missing or inconsistent.
290318 return s .resolveReference (identifier )
291319}
292320
@@ -312,8 +340,14 @@ func (s *Store) resolveReference(reference string) (string, error) {
312340 return "" , fmt .Errorf ("reading reference file: %w" , err )
313341 }
314342
315- // The file content is expected to be the digest string.
316- return strings .TrimSpace (string (data )), nil
343+ // The file content is expected to be the digest string. Refs files are
344+ // generated by us, but we validate defense-in-depth in case the refs
345+ // directory is ever tampered with.
346+ digest := strings .TrimSpace (string (data ))
347+ if err := validateDigest (digest ); err != nil {
348+ return "" , fmt .Errorf ("ref %q: %w" , reference , err )
349+ }
350+ return digest , nil
317351}
318352
319353// createReferenceLink creates a link from reference to digest
0 commit comments