Harden Windows package paths (T-605)
This commit is contained in:
@@ -83,6 +83,70 @@ func TestExtractorRejectsAttackArchives(t *testing.T) {
|
||||
limits: testLimits(),
|
||||
wantErr: ErrPathEscape,
|
||||
},
|
||||
{
|
||||
name: "Windows normalized dot dot traversal",
|
||||
entries: appendEntries(base,
|
||||
testZIPEntry{name: "payload/.. /escape.exe", body: []byte("x")}),
|
||||
entrypoint: "App.exe",
|
||||
limits: testLimits(),
|
||||
wantErr: ErrPathEscape,
|
||||
},
|
||||
{
|
||||
name: "trailing period",
|
||||
entries: appendEntries(base,
|
||||
testZIPEntry{name: "payload/evil.exe.", body: []byte("x")}),
|
||||
entrypoint: "App.exe",
|
||||
limits: testLimits(),
|
||||
wantErr: ErrPathEscape,
|
||||
},
|
||||
{
|
||||
name: "trailing space",
|
||||
entries: appendEntries(base,
|
||||
testZIPEntry{name: "payload/evil.exe ", body: []byte("x")}),
|
||||
entrypoint: "App.exe",
|
||||
limits: testLimits(),
|
||||
wantErr: ErrPathEscape,
|
||||
},
|
||||
{
|
||||
name: "leading space",
|
||||
entries: appendEntries(base,
|
||||
testZIPEntry{name: "payload/ evil.exe", body: []byte("x")}),
|
||||
entrypoint: "App.exe",
|
||||
limits: testLimits(),
|
||||
wantErr: ErrPathEscape,
|
||||
},
|
||||
{
|
||||
name: "reserved device",
|
||||
entries: appendEntries(base,
|
||||
testZIPEntry{name: "payload/NUL.txt", body: []byte("x")}),
|
||||
entrypoint: "App.exe",
|
||||
limits: testLimits(),
|
||||
wantErr: ErrPathEscape,
|
||||
},
|
||||
{
|
||||
name: "reserved device with space before extension",
|
||||
entries: appendEntries(base,
|
||||
testZIPEntry{name: "payload/NUL .txt", body: []byte("x")}),
|
||||
entrypoint: "App.exe",
|
||||
limits: testLimits(),
|
||||
wantErr: ErrPathEscape,
|
||||
},
|
||||
{
|
||||
name: "reserved device superscript",
|
||||
entries: appendEntries(base,
|
||||
testZIPEntry{name: "payload/COM¹.log", body: []byte("x")}),
|
||||
entrypoint: "App.exe",
|
||||
limits: testLimits(),
|
||||
wantErr: ErrPathEscape,
|
||||
},
|
||||
{
|
||||
name: "forbidden Windows character",
|
||||
entries: appendEntries(base,
|
||||
testZIPEntry{name: "payload/evil?.exe", body: []byte("x")}),
|
||||
entrypoint: "App.exe",
|
||||
limits: testLimits(),
|
||||
wantErr: ErrPathEscape,
|
||||
},
|
||||
{
|
||||
name: "backslash traversal",
|
||||
entries: appendEntries(base,
|
||||
@@ -127,6 +191,17 @@ func TestExtractorRejectsAttackArchives(t *testing.T) {
|
||||
limits: testLimits(),
|
||||
wantErr: ErrDuplicateEntry,
|
||||
},
|
||||
{
|
||||
name: "Unicode folded duplicate",
|
||||
entries: []testZIPEntry{
|
||||
{name: "app.json", body: []byte(`{}`)},
|
||||
{name: "payload/K.exe", body: []byte("one")},
|
||||
{name: "payload/K.exe", body: []byte("two")},
|
||||
},
|
||||
entrypoint: "K.exe",
|
||||
limits: testLimits(),
|
||||
wantErr: ErrDuplicateEntry,
|
||||
},
|
||||
{
|
||||
name: "unexpected top level entry",
|
||||
entries: appendEntries(base,
|
||||
@@ -184,7 +259,8 @@ func TestExtractorRejectsAttackArchives(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
archivePath := writeTestZIP(t, test.entries)
|
||||
destination := filepath.Join(t.TempDir(), "staging")
|
||||
root := t.TempDir()
|
||||
destination := filepath.Join(root, "staging")
|
||||
extractor := mustExtractor(t, test.limits)
|
||||
|
||||
_, err := extractor.ExtractFile(archivePath, destination, test.entrypoint)
|
||||
@@ -194,6 +270,9 @@ func TestExtractorRejectsAttackArchives(t *testing.T) {
|
||||
if _, statErr := os.Stat(destination); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("rejected archive left staging, stat error = %v", statErr)
|
||||
}
|
||||
if _, statErr := os.Stat(filepath.Join(root, "escape.exe")); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("rejected archive wrote outside staging, stat error = %v", statErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -210,6 +289,12 @@ func TestExtractorRejectsInvalidEntrypoints(t *testing.T) {
|
||||
{entrypoint: "../App.exe", wantErr: ErrEntrypointInvalid},
|
||||
{entrypoint: "/App.exe", wantErr: ErrEntrypointInvalid},
|
||||
{entrypoint: `..\App.exe`, wantErr: ErrEntrypointInvalid},
|
||||
{entrypoint: ".. /App.exe", wantErr: ErrEntrypointInvalid},
|
||||
{entrypoint: "App.exe.", wantErr: ErrEntrypointInvalid},
|
||||
{entrypoint: "App.exe ", wantErr: ErrEntrypointInvalid},
|
||||
{entrypoint: " App.exe", wantErr: ErrEntrypointInvalid},
|
||||
{entrypoint: "NUL", wantErr: ErrEntrypointInvalid},
|
||||
{entrypoint: "bad?.exe", wantErr: ErrEntrypointInvalid},
|
||||
{entrypoint: "payload/App.exe", wantErr: ErrEntrypointInvalid},
|
||||
{entrypoint: "Missing.exe", wantErr: ErrEntrypointMissing},
|
||||
}
|
||||
@@ -229,6 +314,27 @@ func TestExtractorRejectsInvalidEntrypoints(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractorAcceptsUnicodeNestedPaths(t *testing.T) {
|
||||
archivePath := writeTestZIP(t, []testZIPEntry{
|
||||
{name: "app.json", body: []byte(`{}`)},
|
||||
{name: "payload/工具/解析器.exe", body: []byte("executable")},
|
||||
{name: "payload/资源/说明.txt", body: []byte("说明")},
|
||||
})
|
||||
destination := filepath.Join(t.TempDir(), "staging")
|
||||
extractor := mustExtractor(t, testLimits())
|
||||
|
||||
result, err := extractor.ExtractFile(archivePath, destination, "工具/解析器.exe")
|
||||
if err != nil {
|
||||
t.Fatalf("ExtractFile() error = %v", err)
|
||||
}
|
||||
if result.Files != 2 {
|
||||
t.Fatalf("Files = %d, want 2", result.Files)
|
||||
}
|
||||
if _, err := os.Stat(result.EntrypointPath); err != nil {
|
||||
t.Fatalf("entrypoint stat error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractorRejectsExistingDestination(t *testing.T) {
|
||||
archivePath := writeTestZIP(t, []testZIPEntry{
|
||||
{name: "app.json", body: []byte(`{}`)},
|
||||
|
||||
Reference in New Issue
Block a user