Skip to content

Commit 40c19fa

Browse files
committed
distribution: prevent uncontrolled data used in path expression
Signed-off-by: Dorin Geman <dorin.geman@docker.com>
1 parent 8e257d7 commit 40c19fa

File tree

4 files changed

+132
-26
lines changed

4 files changed

+132
-26
lines changed

pkg/distribution/internal/store/blobs.go

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
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
1954
func (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

2876
type 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.
56108
func (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.
81140
func (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
}

pkg/distribution/internal/store/blobs_test.go

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ func TestBlobs(t *testing.T) {
4141
}
4242

4343
// ensure blob file exists
44-
content, err := os.ReadFile(store.blobPath(hash))
44+
blobPath, err := store.blobPath(hash)
45+
if err != nil {
46+
t.Fatalf("error getting blob path: %v", err)
47+
}
48+
content, err := os.ReadFile(blobPath)
4549
if err != nil {
4650
t.Fatalf("error reading blob file: %v", err)
4751
}
@@ -52,7 +56,11 @@ func TestBlobs(t *testing.T) {
5256
}
5357

5458
// ensure incomplete blob file does not exist
55-
tmpFile := incompletePath(store.blobPath(hash))
59+
blobPath, err = store.blobPath(hash)
60+
if err != nil {
61+
t.Fatalf("error getting blob path: %v", err)
62+
}
63+
tmpFile := incompletePath(blobPath)
5664
if _, err := os.Stat(tmpFile); !errors.Is(err, os.ErrNotExist) {
5765
t.Fatalf("expected incomplete blob file %s not be present", tmpFile)
5866
}
@@ -61,10 +69,14 @@ func TestBlobs(t *testing.T) {
6169
t.Run("WriteBlob fails", func(t *testing.T) {
6270
// simulate lingering incomplete blob file (if program crashed)
6371
hash := v1.Hash{
64-
Algorithm: "some-alg",
65-
Hex: "some-hash",
72+
Algorithm: "sha256",
73+
Hex: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
74+
}
75+
blobPath, err := store.blobPath(hash)
76+
if err != nil {
77+
t.Fatalf("error getting blob path: %v", err)
6678
}
67-
if err := writeFile(incompletePath(store.blobPath(hash)), []byte("incomplete")); err != nil {
79+
if err := writeFile(incompletePath(blobPath), []byte("incomplete")); err != nil {
6880
t.Fatalf("error creating incomplete blob file for test: %v", err)
6981
}
7082

@@ -73,21 +85,29 @@ func TestBlobs(t *testing.T) {
7385
}
7486

7587
// ensure blob file does not exist
76-
if _, err := os.ReadFile(store.blobPath(hash)); !errors.Is(err, os.ErrNotExist) {
88+
blobPath2, err := store.blobPath(hash)
89+
if err != nil {
90+
t.Fatalf("error getting blob path: %v", err)
91+
}
92+
if _, err := os.ReadFile(blobPath2); !errors.Is(err, os.ErrNotExist) {
7793
t.Fatalf("expected blob file not to exist")
7894
}
7995

8096
// ensure incomplete file is not left behind
81-
if _, err := os.ReadFile(incompletePath(store.blobPath(hash))); !errors.Is(err, os.ErrNotExist) {
97+
blobPath3, err := store.blobPath(hash)
98+
if err != nil {
99+
t.Fatalf("error getting blob path: %v", err)
100+
}
101+
if _, err := os.ReadFile(incompletePath(blobPath3)); !errors.Is(err, os.ErrNotExist) {
82102
t.Fatalf("expected incomplete blob file not to exist")
83103
}
84104
})
85105

86106
t.Run("WriteBlob reuses existing blob", func(t *testing.T) {
87107
// simulate existing blob
88108
hash := v1.Hash{
89-
Algorithm: "some-alg",
90-
Hex: "some-hash",
109+
Algorithm: "sha256",
110+
Hex: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
91111
}
92112

93113
if err := store.WriteBlob(hash, bytes.NewReader([]byte("some-data"))); err != nil {
@@ -99,7 +119,11 @@ func TestBlobs(t *testing.T) {
99119
}
100120

101121
// ensure blob file exists
102-
content, err := os.ReadFile(store.blobPath(hash))
122+
blobPath4, err := store.blobPath(hash)
123+
if err != nil {
124+
t.Fatalf("error getting blob path: %v", err)
125+
}
126+
content, err := os.ReadFile(blobPath4)
103127
if err != nil {
104128
t.Fatalf("error reading blob file: %v", err)
105129
}

pkg/distribution/internal/store/manifests.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package store
22

33
import (
44
"bytes"
5-
"errors"
65
"fmt"
76
"os"
87
"path/filepath"
@@ -26,8 +25,12 @@ func (s *LocalStore) WriteManifest(hash v1.Hash, raw []byte) error {
2625
return fmt.Errorf("parse manifest: %w", err)
2726
}
2827
for _, layer := range manifest.Layers {
29-
if !s.hasBlob(layer.Digest) {
30-
return errors.New("missing blob %q for manifest - refusing to write unless all blobs exist")
28+
hasBlob, err := s.hasBlob(layer.Digest)
29+
if err != nil {
30+
return fmt.Errorf("check blob existence: %w", err)
31+
}
32+
if !hasBlob {
33+
return fmt.Errorf("missing blob %q for manifest - refusing to write unless all blobs exist", layer.Digest)
3134
}
3235
}
3336
if err := writeFile(s.manifestPath(hash), raw); err != nil {

pkg/distribution/internal/store/model.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,23 @@ func (s *LocalStore) newModel(digest v1.Hash, tags []string) (*Model, error) {
3535
return nil, fmt.Errorf("parse manifest: %w", err)
3636
}
3737

38-
rawConfigFile, err := os.ReadFile(s.blobPath(manifest.Config.Digest))
38+
configPath, err := s.blobPath(manifest.Config.Digest)
39+
if err != nil {
40+
return nil, fmt.Errorf("get config blob path: %w", err)
41+
}
42+
rawConfigFile, err := os.ReadFile(configPath)
3943
if err != nil {
4044
return nil, fmt.Errorf("read config file: %w", err)
4145
}
4246

4347
layers := make([]v1.Layer, len(manifest.Layers))
4448
for i, ld := range manifest.Layers {
49+
layerPath, err := s.blobPath(ld.Digest)
50+
if err != nil {
51+
return nil, fmt.Errorf("get layer blob path: %w", err)
52+
}
4553
layers[i] = &mdpartial.Layer{
46-
Path: s.blobPath(ld.Digest),
54+
Path: layerPath,
4755
Descriptor: ld,
4856
}
4957
}

0 commit comments

Comments
 (0)