-
Notifications
You must be signed in to change notification settings - Fork 5
fix(security): write ACE artifacts owner-only instead of trusting the… #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| // /////////////////////////////////////////////////////////////////////////// | ||
| // | ||
| // # ACE - Active Consistency Engine | ||
| // | ||
| // Copyright (C) 2023 - 2026, pgEdge (https://www.pgedge.com/) | ||
| // | ||
| // This software is released under the PostgreSQL License: | ||
| // https://opensource.org/license/postgresql | ||
| // | ||
| // /////////////////////////////////////////////////////////////////////////// | ||
|
|
||
| package common | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| ) | ||
|
|
||
| // ACE writes row data copied straight out of the compared tables: diff JSON, | ||
| // HTML reports, repair reports, stale-skip logs. os.Create and os.WriteFile | ||
| // only *request* a mode, which the umask then masks off — under the usual 0022 | ||
| // that lands at 0644, readable by every local user. These helpers set the mode | ||
| // explicitly so it does not depend on the operator's umask. | ||
| const ( | ||
| SecureFileMode os.FileMode = 0o600 | ||
| SecureDirMode os.FileMode = 0o700 | ||
|
Comment on lines
+25
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- secure_file.go ---'
cat -n pkg/common/secure_file.go
printf '%s\n' '--- module and platform indicators ---'
cat go.mod | sed -n '1,40p'
rg -n --glob '!vendor/**' --glob '!node_modules/**' 'GOOS|windows|Windows|syscall|x/sys/windows|ACL|os\.Chmod|SecureFileMode|SecureDirMode' .github Makefile* go.mod go.sum README.md docs pkg internal 2>/dev/null | head -250Repository: pgEdge/ace Length of output: 8433 🌐 Web query:
💡 Result: In the context of the Go programming language, the chmod operation on Windows is implemented differently than on Unix-like systems due to the lack of a traditional POSIX permission model [1]. When using the os.Chmod function on Windows, only the 0o200 bit (owner writable) of the provided mode argument is utilized [1]. This specific bit controls the file's read-only attribute: 1. Setting the bit (e.g., using 0o600 or 0o200) clears the read-only attribute, making the file writable [1]. 2. Clearing the bit (e.g., using 0o400) sets the read-only attribute on the file [1]. All other mode bits are ignored on Windows [1]. For compatibility with older versions of Go (1.12 and earlier), it is recommended to pass a non-zero mode, such as 0o400 for read-only or 0o600 for read-write access [1]. Citations: Security Misconfiguration Reachability: External Use Windows ACLs for secure artifacts. ACE supports Windows, but 🤖 Prompt for AI AgentsSource: MCP tools |
||
| ) | ||
|
|
||
| // CreateFileSecure creates or truncates path for writing, owner-only. The | ||
| // Chmod is not redundant: O_CREATE's mode is umask-masked, and ignored | ||
| // altogether when the file already exists. | ||
| func CreateFileSecure(path string) (*os.File, error) { | ||
| f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, SecureFileMode) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- secure_file.go ---'
cat -n pkg/common/secure_file.go
printf '%s\n' '--- WriteDiffReport and helper caller context ---'
sed -n '1500,1585p' pkg/common/utils.go
printf '%s\n' '--- supported-platform indicators ---'
rg -n --glob 'go.mod' --glob 'README*' --glob '*.yml' --glob '*.yaml' 'windows|Windows|GOOS|linux|darwin' .Repository: pgEdge/ace Length of output: 6014 🤖 get_repo_knowledge executed:
Length of output: 46300 🌐 Web query:
💡 Result: Windows does not have a native chmod command, as its file system permission model differs significantly from Unix-based systems. Instead of chmod, Windows uses the attrib command-line tool to manage file attributes, including the read-only bit. To set or remove the read-only attribute on a file using the Windows command line, you use the attrib command: To set a file to read-only: attrib +r filename To remove the read-only attribute: attrib -r filename The attrib command operates by toggling specific attributes (where + adds and - removes) rather than setting numeric file mode bits as chmod does in Unix environments. Permissions in Windows are more complex and are generally managed via Access Control Lists (ACLs) using tools like icacls, rather than simple read/write/execute mode bits. Citations:
Path Traversal Reachability: External Harden secure output creation on all supported platforms.
🤖 Prompt for AI AgentsSource: MCP tools |
||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if err := f.Chmod(SecureFileMode); err != nil { | ||
| f.Close() | ||
| return nil, fmt.Errorf("restrict permissions on %s: %w", path, err) | ||
| } | ||
| return f, nil | ||
| } | ||
|
|
||
| // WriteFileSecure is os.WriteFile for files that may contain table data. | ||
| func WriteFileSecure(path string, data []byte) error { | ||
| f, err := CreateFileSecure(path) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if _, err := f.Write(data); err != nil { | ||
| f.Close() | ||
| return err | ||
| } | ||
| return f.Close() | ||
| } | ||
|
|
||
| // MkdirAllSecure creates path and any missing parents, restricting path itself | ||
| // to the owner. Only the leaf is tightened, so an existing reports/ stays as | ||
| // the operator set it; the files written underneath are owner-only anyway. | ||
| func MkdirAllSecure(path string) error { | ||
| if err := os.MkdirAll(path, SecureDirMode); err != nil { | ||
| return err | ||
| } | ||
| if err := os.Chmod(path, SecureDirMode); err != nil { | ||
| return fmt.Errorf("restrict permissions on %s: %w", path, err) | ||
| } | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| // /////////////////////////////////////////////////////////////////////////// | ||
| // | ||
| // # ACE - Active Consistency Engine | ||
| // | ||
| // Copyright (C) 2023 - 2026, pgEdge (https://www.pgedge.com/) | ||
| // | ||
| // This software is released under the PostgreSQL License: | ||
| // https://opensource.org/license/postgresql | ||
| // | ||
| // /////////////////////////////////////////////////////////////////////////// | ||
|
|
||
| package common | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/pgedge/ace/pkg/types" | ||
| ) | ||
|
|
||
| func assertOwnerOnly(t *testing.T, path string) { | ||
| t.Helper() | ||
| st, err := os.Stat(path) | ||
| if err != nil { | ||
| t.Fatalf("stat %s: %v", path, err) | ||
| } | ||
| if st.Mode().Perm()&0o077 != 0 { | ||
| t.Errorf("%s has mode %v, want no group/other bits", path, st.Mode().Perm()) | ||
| } | ||
| } | ||
|
|
||
| func TestWriteFileSecure(t *testing.T) { | ||
| path := filepath.Join(t.TempDir(), "diff.json") | ||
| if err := WriteFileSecure(path, []byte("rows")); err != nil { | ||
| t.Fatalf("WriteFileSecure: %v", err) | ||
| } | ||
| assertOwnerOnly(t, path) | ||
|
|
||
| got, err := os.ReadFile(path) | ||
| if err != nil { | ||
| t.Fatalf("read back: %v", err) | ||
| } | ||
| if string(got) != "rows" { | ||
| t.Errorf("content = %q, want %q", got, "rows") | ||
| } | ||
| } | ||
|
|
||
| // A file an older ACE build left world-readable must be tightened on rewrite: | ||
| // O_CREATE's mode is ignored for an existing file, so a stale 0644 would | ||
| // otherwise survive. | ||
| func TestWriteFileSecureTightensExistingFile(t *testing.T) { | ||
| path := filepath.Join(t.TempDir(), "stale.json") | ||
| if err := os.WriteFile(path, []byte("old"), 0o644); err != nil { | ||
| t.Fatalf("seed: %v", err) | ||
| } | ||
| if err := os.Chmod(path, 0o644); err != nil { | ||
| t.Fatalf("seed mode: %v", err) | ||
| } | ||
| if err := WriteFileSecure(path, []byte("new")); err != nil { | ||
| t.Fatalf("WriteFileSecure: %v", err) | ||
| } | ||
| assertOwnerOnly(t, path) | ||
| } | ||
|
|
||
| func TestMkdirAllSecure(t *testing.T) { | ||
| dir := filepath.Join(t.TempDir(), "reports", "2026-01-01") | ||
| if err := MkdirAllSecure(dir); err != nil { | ||
| t.Fatalf("MkdirAllSecure: %v", err) | ||
| } | ||
| assertOwnerOnly(t, dir) | ||
| } | ||
|
|
||
| // The real writer: WriteDiffReport emits row data, so neither the JSON nor the | ||
| // HTML report may be readable by other local users. | ||
| func TestWriteDiffReportIsNotWorldReadable(t *testing.T) { | ||
| for _, format := range []string{"json", "html"} { | ||
| t.Run(format, func(t *testing.T) { | ||
| cwd, err := os.Getwd() | ||
| if err != nil { | ||
| t.Fatalf("getwd: %v", err) | ||
| } | ||
| if err := os.Chdir(t.TempDir()); err != nil { | ||
| t.Fatalf("chdir: %v", err) | ||
| } | ||
| t.Cleanup(func() { _ = os.Chdir(cwd) }) | ||
|
|
||
| diff := types.DiffOutput{ | ||
| NodeDiffs: map[string]types.DiffByNodePair{ | ||
| "n1/n2": {Rows: map[string][]types.OrderedMap{"n1": {}, "n2": {}}}, | ||
| }, | ||
| Summary: types.DiffSummary{ | ||
| Schema: "public", Table: "customers", | ||
| Nodes: []string{"n1", "n2"}, PrimaryKey: []string{"id"}, | ||
| DiffRowsCount: map[string]int{"n1/n2": 0}, | ||
| }, | ||
| } | ||
|
|
||
| jsonPath, htmlPath, err := WriteDiffReport(diff, "public", "customers", format) | ||
| if err != nil { | ||
| t.Fatalf("WriteDiffReport: %v", err) | ||
| } | ||
| assertOwnerOnly(t, jsonPath) | ||
| if format == "html" { | ||
| assertOwnerOnly(t, htmlPath) | ||
| } | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // /////////////////////////////////////////////////////////////////////////// | ||
| // | ||
| // # ACE - Active Consistency Engine | ||
| // | ||
| // Copyright (C) 2023 - 2026, pgEdge (https://www.pgedge.com/) | ||
| // | ||
| // This software is released under the PostgreSQL License: | ||
| // https://opensource.org/license/postgresql | ||
| // | ||
| // /////////////////////////////////////////////////////////////////////////// | ||
|
|
||
| //go:build unix | ||
|
|
||
| package common | ||
|
|
||
| import ( | ||
| "path/filepath" | ||
| "syscall" | ||
| "testing" | ||
| ) | ||
|
|
||
| // The defect was that the mode came from the ambient umask rather than from | ||
| // ACE. Pinning the umask wide open isolates that. Not parallel: umask is | ||
| // process-global. | ||
| func TestSecureWritesIgnoreUmask(t *testing.T) { | ||
| old := syscall.Umask(0) | ||
| t.Cleanup(func() { syscall.Umask(old) }) | ||
|
|
||
| dir := t.TempDir() | ||
|
|
||
| file := filepath.Join(dir, "diff.json") | ||
| if err := WriteFileSecure(file, []byte("rows")); err != nil { | ||
| t.Fatalf("WriteFileSecure: %v", err) | ||
| } | ||
| assertOwnerOnly(t, file) | ||
|
|
||
| sub := filepath.Join(dir, "reports", "2026-01-01") | ||
| if err := MkdirAllSecure(sub); err != nil { | ||
| t.Fatalf("MkdirAllSecure: %v", err) | ||
| } | ||
| assertOwnerOnly(t, sub) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: pgEdge/ace
Length of output: 6748
🏁 Script executed:
Repository: pgEdge/ace
Length of output: 6717
🤖 get_repo_knowledge executed:
get_repo_knowledge pgEdge/ace /tmp/coderabbit-repo-knowledge/pgedge-ace-fe7e1d3d/learnings /tmp/coderabbit-repo-knowledge/pgedge-ace-fe7e1d3d/architectureLength of output: 7410
Sensitive Data Exposure
Reachability: External
Exploitability: Moderate
CWE: CWE-732 — Incorrect Permission Assignment for Critical Resource
Set the mode before writing credential content.
When
--forceoverwrites an existing permissivepg_service.conf,os.WriteFilewrites credentials beforeos.Chmodrestricts access. Open the file without truncating it, setperm, truncate it, and then write the content.🤖 Prompt for AI Agents