fix(R-1187 b): refuse a link whose parent path passes through a link; born keys listed; psql --set
gates / gates (push) Successful in 1m7s

Second automated security review finding, red-proved. The go-parity and docker -v gates that refused the push are green.
This commit is contained in:
2026-10-26 20:30:34 +01:00
parent 106325de43
commit f24233c829
4 changed files with 70 additions and 3 deletions
@@ -186,7 +186,7 @@ func TestR1187g_PostgisIsPostgresAndPsqlStopsOnError(t *testing.T) {
}
}
args := strings.Join(psqlImportArgs("c", "u", "d"), " ")
if !strings.Contains(args, "-v ON_ERROR_STOP=1") {
if !strings.Contains(args, "--set=ON_ERROR_STOP=1") {
t.Fatalf("psql does not stop on error: %s", args)
}
}
@@ -216,3 +216,35 @@ func TestR1187b_NoWriteThroughALinkThatLeavesTheFolder(t *testing.T) {
t.Fatal("a file was written OUTSIDE the app's folder through a pre-existing link")
}
}
// (b), the security review's second case — a parser differential: link „a" → „." makes „a/b" really „b", so a later
// link „a/b/l" → „../.." passes a path-TEXT check (root/a/b/../.. = root) while it really points one level ABOVE the
// app's folder. A link entry whose parent path passes through a link is refused and listed. RED-PROOF: with the text
// check only, „a/b/l" is created and points outside.
func TestR1187b_NoLinkThroughALinkedParent(t *testing.T) {
dest := t.TempDir()
tarPath := filepath.Join(t.TempDir(), "d.tar")
f, _ := os.Create(tarPath)
tw := tar.NewWriter(f)
tw.WriteHeader(&tar.Header{Name: "a", Typeflag: tar.TypeSymlink, Linkname: "."})
tw.WriteHeader(&tar.Header{Name: "a/b/", Typeflag: tar.TypeDir, Mode: 0o755})
tw.WriteHeader(&tar.Header{Name: "a/b/l", Typeflag: tar.TypeSymlink, Linkname: "../.."})
tw.Close()
f.Close()
old := lchownFn
lchownFn = func(*os.Root, string, int, int) error { return nil }
defer func() { lchownFn = old }()
refused, err := extractTar(tarPath, dest)
if err != nil {
t.Fatal(err)
}
for _, p := range []string{filepath.Join(dest, "b", "l"), filepath.Join(dest, "a", "b", "l")} {
if _, err := os.Lstat(p); err == nil {
real, _ := filepath.EvalSymlinks(p)
t.Fatalf("a link through a linked parent was created (%s → %s)", p, real)
}
}
if !strings.Contains(strings.Join(refused, ","), "a/b/l") {
t.Fatalf("the refused link must be listed, got %v", refused)
}
}
+28 -1
View File
@@ -1087,7 +1087,7 @@ func waitForDB(containerID, dbType string, env map[string]string) error {
func psqlImportArgs(containerID, user, dbName string) []string {
// R-1187 (g): stop on the first error — the dump is --clean --if-exists, so a failing statement is a real failure,
// and without ON_ERROR_STOP psql went on and the import reported success over a half-loaded database.
return []string{"exec", "-i", containerID, "psql", "-v", "ON_ERROR_STOP=1", "-U", user, "-d", dbName}
return []string{"exec", "-i", containerID, "psql", "--set=ON_ERROR_STOP=1", "-U", user, "-d", dbName}
}
// importDBDump imports a (possibly gzipped) SQL dump into the running database container.
@@ -1203,6 +1203,26 @@ func extractTarGz(tgzPath, destDir string) error {
}
}
// parentThroughLink is true when any folder on name's path (inside the root) is a symbolic link.
func parentThroughLink(rt *os.Root, name string) bool {
dir := filepath.Dir(name)
if dir == "." {
return false
}
acc := ""
for _, part := range strings.Split(dir, string(os.PathSeparator)) {
acc = filepath.Join(acc, part)
fi, err := rt.Lstat(acc)
if err != nil {
return false // not there yet: MkdirAll creates real folders
}
if fi.Mode()&os.ModeSymlink != 0 {
return true
}
}
return false
}
// lchownFn is the owner seam (a test records it; production is os.Lchown).
var lchownFn = func(r *os.Root, name string, uid, gid int) error { return r.Lchown(name, uid, gid) }
@@ -1270,6 +1290,13 @@ func extractTar(tarPath, destDir string) ([]string, error) {
refused = append(refused, hdr.Name)
continue
}
// The text check below is exact only when no folder on the link's path is itself a link (a „a" → „." makes
// „a/b/l" really „b/l"): such an entry is refused (the security review's second case, pinned by
// TestR1187b_NoLinkThroughALinkedParent).
if parentThroughLink(rt, name) {
refused = append(refused, hdr.Name)
continue
}
resolved := filepath.Clean(filepath.Join(filepath.Dir(filepath.Join(root, name)), dst))
if resolved != root && !strings.HasPrefix(resolved, root+string(os.PathSeparator)) {
refused = append(refused, hdr.Name)