From cd3233ffe88ee066ea995edb0de24f0413d0d2dd Mon Sep 17 00:00:00 2001 From: Ilker Manap Date: Wed, 16 Sep 2026 09:01:41 +0200 Subject: [PATCH] Do not leave the key store world-readable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SQLite creates its file with the process umask, which on a stock system means every account on the machine can read it. What is in there is not a working credential — keys are stored hashed — but it is the whole tenancy tree and every request anyone made, and that should not depend on an operator remembering to chmod it. Co-Authored-By: Claude Opus 5 (1M context) --- internal/store/sqlite.go | 12 ++++++++++++ internal/store/store_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go index 5e8a91a8..3bb6d0be 100644 --- a/internal/store/sqlite.go +++ b/internal/store/sqlite.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "os" "slices" "strings" "time" @@ -44,6 +45,17 @@ func OpenSQLite(ctx context.Context, path string) (*SQLite, error) { _ = db.Close() return nil, err } + // SQLite creates the file with the process umask, which on a stock system + // means world-readable. What is in here is not a working credential — keys + // are stored hashed — but it is the whole tenancy tree and every request + // anyone made, and that should not be readable by every account on the + // machine because nobody remembered to chmod it. + for _, f := range []string{path, path + "-wal", path + "-shm"} { + if err := os.Chmod(f, 0o600); err != nil && !os.IsNotExist(err) { + _ = db.Close() + return nil, fmt.Errorf("securing %s: %w", f, err) + } + } return &SQLite{db: db}, nil } diff --git a/internal/store/store_test.go b/internal/store/store_test.go index 60bb3cc6..00a6ae04 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -3,6 +3,7 @@ package store import ( "context" "errors" + "os" "path/filepath" "slices" "strings" @@ -403,3 +404,33 @@ func must(t *testing.T, err error) { t.Fatal(err) } } + +func TestTheStoreIsNotWorldReadable(t *testing.T) { + // SQLite creates the file with the process umask, which on a stock system + // leaves it readable by every account on the machine. What is in here is + // not a working credential, but it is the whole tenancy tree and every + // request anyone made. + dir := t.TempDir() + path := filepath.Join(dir, "llmgw.db") + s, err := OpenSQLite(context.Background(), path) + if err != nil { + t.Fatal(err) + } + defer s.Close() + + // Force the WAL files into existence. + tree(t, s) + + for _, name := range []string{"llmgw.db", "llmgw.db-wal", "llmgw.db-shm"} { + info, err := os.Stat(filepath.Join(dir, name)) + if os.IsNotExist(err) { + continue + } + if err != nil { + t.Fatal(err) + } + if mode := info.Mode().Perm(); mode&0o077 != 0 { + t.Errorf("%s is mode %o", name, mode) + } + } +}