55 "io"
66 "os"
77 "path/filepath"
8+ "strings"
89
910 "github.com/docker/model-runner/pkg/distribution/internal/progress"
1011
@@ -15,14 +16,61 @@ const (
1516 blobsDir = "blobs"
1617)
1718
19+ var allowedAlgorithms = map [string ]int {
20+ "sha256" : 64 ,
21+ "sha512" : 128 ,
22+ }
23+
24+ func isSafeAlgorithm (a string ) (int , bool ) {
25+ hexLength , ok := allowedAlgorithms [a ]
26+ return hexLength , ok
27+ }
28+
29+ func isSafeHex (hexLength int , s string ) bool {
30+ if len (s ) != hexLength {
31+ return false
32+ }
33+ for _ , c := range s {
34+ if ! ((c >= '0' && c <= '9' ) || (c >= 'a' && c <= 'f' ) || (c >= 'A' && c <= 'F' )) {
35+ return false
36+ }
37+ }
38+ return true
39+ }
40+
41+ // validateHash ensures the hash components are safe for filesystem paths
42+ func validateHash (hash v1.Hash ) error {
43+ hexLength , ok := isSafeAlgorithm (hash .Algorithm )
44+ if ! ok {
45+ return fmt .Errorf ("invalid hash algorithm: %q not in allowlist" , hash .Algorithm )
46+ }
47+ if ! isSafeHex (hexLength , hash .Hex ) {
48+ return fmt .Errorf ("invalid hash hex: contains non-hexadecimal characters or invalid length" )
49+ }
50+ return nil
51+ }
52+
1853// blobDir returns the path to the blobs directory
1954func (s * LocalStore ) blobsDir () string {
2055 return filepath .Join (s .rootPath , blobsDir )
2156}
2257
2358// blobPath returns the path to the blob for the given hash.
24- func (s * LocalStore ) blobPath (hash v1.Hash ) string {
25- return filepath .Join (s .rootPath , blobsDir , hash .Algorithm , hash .Hex )
59+ func (s * LocalStore ) blobPath (hash v1.Hash ) (string , error ) {
60+ if err := validateHash (hash ); err != nil {
61+ return "" , fmt .Errorf ("unsafe hash: %w" , err )
62+ }
63+
64+ path := filepath .Join (s .rootPath , blobsDir , hash .Algorithm , hash .Hex )
65+
66+ cleanRootPath := filepath .Clean (s .rootPath )
67+ cleanPath := filepath .Clean (path )
68+ relPath , err := filepath .Rel (cleanRootPath , cleanPath )
69+ if err != nil || strings .HasPrefix (relPath , ".." ) {
70+ return "" , fmt .Errorf ("path traversal attempt detected: %s" , path )
71+ }
72+
73+ return cleanPath , nil
2674}
2775
2876type blob interface {
@@ -36,7 +84,11 @@ func (s *LocalStore) writeLayer(layer blob, updates chan<- v1.Update) error {
3684 if err != nil {
3785 return fmt .Errorf ("get file hash: %w" , err )
3886 }
39- if s .hasBlob (hash ) {
87+ hasBlob , err := s .hasBlob (hash )
88+ if err != nil {
89+ return fmt .Errorf ("check blob existence: %w" , err )
90+ }
91+ if hasBlob {
4092 // todo: write something to the progress channel (we probably need to redo progress reporting a little bit)
4193 return nil
4294 }
@@ -54,11 +106,18 @@ func (s *LocalStore) writeLayer(layer blob, updates chan<- v1.Update) error {
54106// WriteBlob writes the blob to the store, reporting progress to the given channel.
55107// If the blob is already in the store, it is a no-op and the blob is not consumed from the reader.
56108func (s * LocalStore ) WriteBlob (diffID v1.Hash , r io.Reader ) error {
57- if s .hasBlob (diffID ) {
109+ hasBlob , err := s .hasBlob (diffID )
110+ if err != nil {
111+ return fmt .Errorf ("check blob existence: %w" , err )
112+ }
113+ if hasBlob {
58114 return nil
59115 }
60116
61- path := s .blobPath (diffID )
117+ path , err := s .blobPath (diffID )
118+ if err != nil {
119+ return fmt .Errorf ("get blob path: %w" , err )
120+ }
62121 f , err := createFile (incompletePath (path ))
63122 if err != nil {
64123 return fmt .Errorf ("create blob file: %w" , err )
@@ -79,14 +138,22 @@ func (s *LocalStore) WriteBlob(diffID v1.Hash, r io.Reader) error {
79138
80139// removeBlob removes the blob with the given hash from the store.
81140func (s * LocalStore ) removeBlob (hash v1.Hash ) error {
82- return os .Remove (s .blobPath (hash ))
141+ path , err := s .blobPath (hash )
142+ if err != nil {
143+ return fmt .Errorf ("get blob path: %w" , err )
144+ }
145+ return os .Remove (path )
83146}
84147
85- func (s * LocalStore ) hasBlob (hash v1.Hash ) bool {
86- if _ , err := os .Stat (s .blobPath (hash )); err == nil {
87- return true
148+ func (s * LocalStore ) hasBlob (hash v1.Hash ) (bool , error ) {
149+ path , err := s .blobPath (hash )
150+ if err != nil {
151+ return false , fmt .Errorf ("get blob path: %w" , err )
88152 }
89- return false
153+ if _ , err := os .Stat (path ); err == nil {
154+ return true , nil
155+ }
156+ return false , nil
90157}
91158
92159// createFile is a wrapper around os.Create that creates any parent directories as needed.
@@ -112,5 +179,9 @@ func (s *LocalStore) writeConfigFile(mdl v1.Image) error {
112179 if err != nil {
113180 return fmt .Errorf ("get raw manifest: %w" , err )
114181 }
115- return writeFile (s .blobPath (hash ), rcf )
182+ path , err := s .blobPath (hash )
183+ if err != nil {
184+ return fmt .Errorf ("get blob path: %w" , err )
185+ }
186+ return writeFile (path , rcf )
116187}
0 commit comments