Skip to content

Commit c130a53

Browse files
authored
Merge pull request #6910 from tonistiigi/platforms-id-sanitize
exporter: sanitize platform IDs in paths
2 parents 59eeaf8 + 77b65ab commit c130a53

6 files changed

Lines changed: 153 additions & 5 deletions

File tree

client/client_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"path"
2424
"path/filepath"
2525
"runtime"
26+
"sort"
2627
"strconv"
2728
"strings"
2829
"sync"
@@ -246,6 +247,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
246247
testExportLocalNoPlatformSplit,
247248
testExportLocalNoPlatformSplitOverwrite,
248249
testExportLocalForcePlatformSplit,
250+
testExportTarPlatformIDSanitized,
249251
testExportLocalModeCopyKeepsStaleDestinationFiles,
250252
testExportLocalModeDeleteRemovesStaleDestinationFiles,
251253
testExportLocalModeCopyMultiPlatformKeepsAllPlatforms,
@@ -8093,6 +8095,90 @@ func testExportLocalForcePlatformSplit(t *testing.T, sb integration.Sandbox) {
80938095
require.Equal(t, "hello", string(dt))
80948096
}
80958097

8098+
func testExportTarPlatformIDSanitized(t *testing.T, sb integration.Sandbox) {
8099+
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureMultiPlatform)
8100+
c, err := New(sb.Context(), sb.Address())
8101+
require.NoError(t, err)
8102+
defer c.Close()
8103+
8104+
const platformID = `..\buildkit-outside`
8105+
platform := platforms.DefaultSpec()
8106+
8107+
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
8108+
st := llb.Scratch().File(
8109+
llb.Mkfile("payload.txt", 0600, []byte("payload")),
8110+
)
8111+
8112+
def, err := st.Marshal(ctx)
8113+
if err != nil {
8114+
return nil, err
8115+
}
8116+
8117+
r, err := c.Solve(ctx, gateway.SolveRequest{
8118+
Definition: def.ToPB(),
8119+
})
8120+
if err != nil {
8121+
return nil, err
8122+
}
8123+
8124+
ref, err := r.SingleRef()
8125+
if err != nil {
8126+
return nil, err
8127+
}
8128+
8129+
res := gateway.NewResult()
8130+
res.AddRef(platformID, ref)
8131+
8132+
dt, err := json.Marshal(&exptypes.Platforms{
8133+
Platforms: []exptypes.Platform{{
8134+
ID: platformID,
8135+
Platform: platform,
8136+
}},
8137+
})
8138+
if err != nil {
8139+
return nil, err
8140+
}
8141+
res.AddMeta(exptypes.ExporterPlatformsKey, dt)
8142+
8143+
return res, nil
8144+
}
8145+
8146+
outW := bytes.NewBuffer(nil)
8147+
_, err = c.Build(sb.Context(), SolveOpt{
8148+
Exports: []ExportEntry{
8149+
{
8150+
Type: ExporterTar,
8151+
Output: fixedWriteCloser(&iohelper.NopWriteCloser{Writer: outW}),
8152+
},
8153+
},
8154+
}, "", frontend, nil)
8155+
require.NoError(t, err)
8156+
8157+
m, err := testutil.ReadTarToMap(outW.Bytes(), false)
8158+
require.NoError(t, err)
8159+
8160+
for name := range m {
8161+
require.Falsef(t, strings.HasPrefix(name, "../") ||
8162+
strings.HasPrefix(name, `..\`) ||
8163+
strings.Contains(name, `/../`) ||
8164+
strings.Contains(name, `\..\`) ||
8165+
strings.Contains(name, `\`) ||
8166+
strings.Contains(name, ":"),
8167+
"tar exporter emitted unsafe platform path %q", name)
8168+
}
8169+
8170+
tarPaths := make([]string, 0, len(m))
8171+
for name := range m {
8172+
tarPaths = append(tarPaths, name)
8173+
}
8174+
sort.Strings(tarPaths)
8175+
8176+
expectedPath := path.Join(".._buildkit-outside", integration.UnixOrWindows("payload.txt", "Files/payload.txt"))
8177+
item := m[expectedPath]
8178+
require.NotNilf(t, item, "expected sanitized tar path %q in %v", expectedPath, tarPaths)
8179+
require.Equal(t, "payload", string(item.Data))
8180+
}
8181+
80968182
func testExportLocalModeCopyKeepsStaleDestinationFiles(t *testing.T, sb integration.Sandbox) {
80978183
c, err := New(sb.Context(), sb.Address())
80988184
require.NoError(t, err)

exporter/local/export.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package local
33
import (
44
"context"
55
"os"
6-
"strings"
76
"sync"
87
"time"
98

@@ -127,7 +126,7 @@ func (e *localExporterInstance) Export(ctx context.Context, inp *exporter.Source
127126
platformDirStat := func(k string, opt CreateFSOpts) *fstypes.Stat {
128127
st := &fstypes.Stat{
129128
Mode: uint32(os.ModeDir | 0755),
130-
Path: strings.ReplaceAll(k, "/", "_"),
129+
Path: PlatformIDToPath(k),
131130
}
132131
if opt.Epoch != nil && opt.Epoch.Value != nil {
133132
st.ModTime = opt.Epoch.Value.UnixNano()

exporter/local/fs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ func CreateFS(ctx context.Context, sessionID string, k string, ref cache.Immutab
199199
if addPlatformToFilename {
200200
nameExt := path.Ext(name)
201201
namBase := strings.TrimSuffix(name, nameExt)
202-
name = fmt.Sprintf("%s.%s%s", namBase, strings.ReplaceAll(k, "/", "_"), nameExt)
202+
name = fmt.Sprintf("%s.%s%s", namBase, PlatformIDToPath(k), nameExt)
203203
}
204204
if _, ok := names[name]; ok {
205205
return nil, nil, errors.Errorf("duplicate attestation path name %s", name)

exporter/local/platform.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package local
2+
3+
import "strings"
4+
5+
// PlatformIDToPath maps an exporter platform ID to a single path component.
6+
func PlatformIDToPath(id string) string {
7+
id = strings.NewReplacer("/", "_", `\`, "_", ":", "_").Replace(id)
8+
switch id {
9+
case ".", "..":
10+
return strings.Repeat("_", len(id))
11+
default:
12+
return id
13+
}
14+
}

exporter/local/platform_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package local
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestPlatformIDToPath(t *testing.T) {
10+
for _, tc := range []struct {
11+
name string
12+
id string
13+
want string
14+
}{
15+
{
16+
name: "slashes",
17+
id: "linux/amd64",
18+
want: "linux_amd64",
19+
},
20+
{
21+
name: "windows separators",
22+
id: `..\buildkit-outside`,
23+
want: ".._buildkit-outside",
24+
},
25+
{
26+
name: "drive separator",
27+
id: `windows/amd64:C`,
28+
want: "windows_amd64_C",
29+
},
30+
{
31+
name: "dot",
32+
id: ".",
33+
want: "_",
34+
},
35+
{
36+
name: "dot dot",
37+
id: "..",
38+
want: "__",
39+
},
40+
{
41+
name: "embedded dots",
42+
id: "linux.v2/amd64",
43+
want: "linux.v2_amd64",
44+
},
45+
} {
46+
t.Run(tc.name, func(t *testing.T) {
47+
require.Equal(t, tc.want, PlatformIDToPath(tc.id))
48+
})
49+
}
50+
}

exporter/tar/export.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"os"
66
"slices"
7-
"strings"
87
"time"
98

109
"github.com/moby/buildkit/cache"
@@ -109,7 +108,7 @@ func (e *localExporterInstance) Export(ctx context.Context, inp *exporter.Source
109108

110109
st := &fstypes.Stat{
111110
Mode: uint32(os.ModeDir | 0755),
112-
Path: strings.ReplaceAll(k, "/", "_"),
111+
Path: local.PlatformIDToPath(k),
113112
}
114113
if opt.Epoch != nil && opt.Epoch.Value != nil {
115114
st.ModTime = opt.Epoch.Value.UnixNano()

0 commit comments

Comments
 (0)