From 44ffe3f163e2ed7280ffa4afec272056935763e5 Mon Sep 17 00:00:00 2001 From: Nelson Isioma Date: Wed, 21 Oct 2026 21:59:16 +0100 Subject: [PATCH] Restrict permissions on stored credentials file The config file holding the API token was written with the default mode (0644 under a typical umask) inside a 0755 directory, so other local users could read the token when the home directory is not private. Create ~/.ohdear with 0700 and set config.json to 0600 before writing, which also tightens files written by earlier versions on next write. Co-Authored-By: Claude Opus 5.5 --- app/Services/CredentialStore.php | 21 ++++++++++++--------- tests/Feature/CredentialStoreTest.php | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/app/Services/CredentialStore.php b/app/Services/CredentialStore.php index 016e0b4..ef34842 100644 --- a/app/Services/CredentialStore.php +++ b/app/Services/CredentialStore.php @@ -35,20 +35,14 @@ public function setToken(string $token): void $data = $this->readConfig(); $data['token'] = $token; - file_put_contents( - $this->configPath, - json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), - ); + $this->writeConfig(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } public function flush(): void { $this->ensureConfigDirectoryExists(); - file_put_contents( - $this->configPath, - json_encode((object) [], JSON_PRETTY_PRINT), - ); + $this->writeConfig(json_encode((object) [], JSON_PRETTY_PRINT)); } private function ensureConfigDirectoryExists(): void @@ -56,10 +50,19 @@ private function ensureConfigDirectoryExists(): void $directory = dirname($this->configPath); if (! is_dir($directory)) { - mkdir($directory, 0755, true); + mkdir($directory, 0700, true); } } + private function writeConfig(string $contents): void + { + // Restrict permissions before writing so the token is never readable by other users. + touch($this->configPath); + chmod($this->configPath, 0600); + + file_put_contents($this->configPath, $contents, LOCK_EX); + } + /** @return array */ private function readConfig(): array { diff --git a/tests/Feature/CredentialStoreTest.php b/tests/Feature/CredentialStoreTest.php index 5508920..20efe26 100644 --- a/tests/Feature/CredentialStoreTest.php +++ b/tests/Feature/CredentialStoreTest.php @@ -67,3 +67,27 @@ expect($contents)->toContain("\n"); expect(json_decode($contents, true))->toBe(['token' => 'test-token']); }); + +it('creates the config file readable only by the owner', function () { + $this->store->setToken('test-token'); + + expect(fileperms($this->tempDir.'/.ohdear/config.json') & 0777)->toBe(0600); + expect(fileperms($this->tempDir.'/.ohdear') & 0777)->toBe(0700); +})->skipOnWindows(); + +it('restricts permissions of an existing config file', function () { + $configFile = $this->tempDir.'/.ohdear/config.json'; + mkdir(dirname($configFile), 0755); + file_put_contents($configFile, json_encode(['token' => 'old-token'])); + chmod($configFile, 0644); + + $this->store->setToken('new-token'); + + expect(fileperms($configFile) & 0777)->toBe(0600); +})->skipOnWindows(); + +it('keeps the config file readable only by the owner after flushing', function () { + $this->store->flush(); + + expect(fileperms($this->tempDir.'/.ohdear/config.json') & 0777)->toBe(0600); +})->skipOnWindows();