diff --git a/install.sh b/install.sh index 4ff60a069..919024f09 100644 --- a/install.sh +++ b/install.sh @@ -155,7 +155,7 @@ write_install_result() { local u="$1" p="$2" port="$3" wbp="$4" scheme="$5" host="$6" token="$7" dbtype="$8" local result_file="/etc/x-ui/install-result.env" local url_host="${host:-SERVER_IP_UNKNOWN}" - install -d -m 755 /etc/x-ui 2> /dev/null + install -d -m 700 /etc/x-ui 2> /dev/null local prev_umask prev_umask=$(umask) umask 077 diff --git a/internal/database/db.go b/internal/database/db.go index 285fa429a..fd90f4603 100644 --- a/internal/database/db.go +++ b/internal/database/db.go @@ -2094,7 +2094,7 @@ func InitDB(dbPath string) error { } default: dir := path.Dir(dbPath) - if err = os.MkdirAll(dir, 0o755); err != nil { + if err = os.MkdirAll(dir, 0o700); err != nil { return err } if err = cleanupSQLiteBackupDirs(filepath.Dir(dbPath)); err != nil { @@ -2108,6 +2108,9 @@ func InitDB(dbPath string) error { if err != nil { return err } + if err := restrictSQLiteFilePerms(dbPath); err != nil { + log.Printf("restrict SQLite file permissions: %v", err) + } sqlDB, err := db.DB() if err != nil { return err @@ -2213,6 +2216,17 @@ func openPostgresWithRetry(dsn string, c *gorm.Config) (*gorm.DB, error) { return nil, fmt.Errorf("postgres unreachable after %d attempts: %w", len(delays), lastErr) } +// The store holds client secrets, so it and its WAL/SHM side files stay +// owner-only. Best effort: a store the panel cannot chmod still opens. +func restrictSQLiteFilePerms(dbPath string) error { + for _, name := range []string{dbPath, dbPath + "-wal", dbPath + "-shm"} { + if err := os.Chmod(name, 0o600); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + } + return nil +} + func sqliteJournalMode() string { switch strings.ToUpper(strings.TrimSpace(os.Getenv("XUI_DB_JOURNAL_MODE"))) { case "DELETE": diff --git a/internal/database/db_permissions_test.go b/internal/database/db_permissions_test.go new file mode 100644 index 000000000..289a94451 --- /dev/null +++ b/internal/database/db_permissions_test.go @@ -0,0 +1,79 @@ +package database + +import ( + "errors" + "os" + "path/filepath" + "runtime" + "testing" +) + +func TestInitDBRestrictsSQLiteFilePermissions(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("POSIX permission bits are not meaningful on Windows") + } + t.Setenv("XUI_DB_JOURNAL_MODE", "") + dbDir := filepath.Join(t.TempDir(), "x-ui") + dbPath := filepath.Join(dbDir, "x-ui.db") + + if err := InitDB(dbPath); err != nil { + t.Fatalf("InitDB: %v", err) + } + t.Cleanup(func() { _ = CloseDB() }) + + if info, err := os.Stat(dbDir); err != nil { + t.Fatalf("stat db dir: %v", err) + } else if perm := info.Mode().Perm(); perm != 0o700 { + t.Fatalf("db dir perm = %o, want 700", perm) + } + for _, name := range []string{dbPath, dbPath + "-wal", dbPath + "-shm"} { + info, err := os.Stat(name) + if errors.Is(err, os.ErrNotExist) && name != dbPath { + continue + } + if err != nil { + t.Fatalf("stat %s: %v", name, err) + } + if perm := info.Mode().Perm(); perm != 0o600 { + t.Fatalf("%s perm = %o, want 600", filepath.Base(name), perm) + } + } +} + +func TestInitDBTightensExistingSQLiteFilePermissions(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("POSIX permission bits are not meaningful on Windows") + } + t.Setenv("XUI_DB_JOURNAL_MODE", "") + dbPath := filepath.Join(t.TempDir(), "x-ui.db") + if err := InitDB(dbPath); err != nil { + t.Fatalf("seed InitDB: %v", err) + } + if err := CloseDB(); err != nil { + t.Fatalf("seed CloseDB: %v", err) + } + // Simulate a store created by an older release under the default umask. + for _, name := range []string{dbPath, dbPath + "-wal", dbPath + "-shm"} { + if err := os.Chmod(name, 0o644); err != nil && !errors.Is(err, os.ErrNotExist) { + t.Fatalf("chmod %s: %v", name, err) + } + } + + if err := InitDB(dbPath); err != nil { + t.Fatalf("InitDB: %v", err) + } + t.Cleanup(func() { _ = CloseDB() }) + + for _, name := range []string{dbPath, dbPath + "-wal", dbPath + "-shm"} { + info, err := os.Stat(name) + if errors.Is(err, os.ErrNotExist) && name != dbPath { + continue + } + if err != nil { + t.Fatalf("stat %s: %v", name, err) + } + if perm := info.Mode().Perm(); perm != 0o600 { + t.Fatalf("%s perm = %o, want 600", filepath.Base(name), perm) + } + } +} diff --git a/internal/database/dump_sqlite.go b/internal/database/dump_sqlite.go index c37839392..274e6efcf 100644 --- a/internal/database/dump_sqlite.go +++ b/internal/database/dump_sqlite.go @@ -24,7 +24,7 @@ func DumpSQLite(srcPath, outPath string) error { if err != nil { return err } - return os.WriteFile(outPath, data, 0o644) + return os.WriteFile(outPath, data, 0o600) } // DumpSQLiteToBytes builds the same `sqlite3 .dump`-style SQL text as DumpSQLite