diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 3067d98e7..6749d224b 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -2332,7 +2332,7 @@ public function delete_backup_by_uuid(Request $request) } catch (\Exception $e) { DB::rollBack(); - return response()->json(['message' => 'Failed to delete backup: '.$e->getMessage()], 500); + return response()->json(['message' => 'Failed to delete backup.'], 500); } } @@ -2452,7 +2452,7 @@ public function delete_execution_by_uuid(Request $request) 'message' => 'Backup execution deleted.', ]); } catch (\Exception $e) { - return response()->json(['message' => 'Failed to delete backup execution: '.$e->getMessage()], 500); + return response()->json(['message' => 'Failed to delete backup execution.'], 500); } } diff --git a/app/Http/Controllers/Api/HetznerController.php b/app/Http/Controllers/Api/HetznerController.php index ed91b4475..092c48594 100644 --- a/app/Http/Controllers/Api/HetznerController.php +++ b/app/Http/Controllers/Api/HetznerController.php @@ -121,7 +121,7 @@ public function locations(Request $request) return response()->json($locations); } catch (\Throwable $e) { - return response()->json(['message' => 'Failed to fetch locations: '.$e->getMessage()], 500); + return response()->json(['message' => 'Failed to fetch Hetzner locations.'], 500); } } @@ -242,7 +242,7 @@ public function serverTypes(Request $request) return response()->json($serverTypes); } catch (\Throwable $e) { - return response()->json(['message' => 'Failed to fetch server types: '.$e->getMessage()], 500); + return response()->json(['message' => 'Failed to fetch Hetzner server types.'], 500); } } @@ -354,7 +354,7 @@ public function images(Request $request) return response()->json(array_values($filtered)); } catch (\Throwable $e) { - return response()->json(['message' => 'Failed to fetch images: '.$e->getMessage()], 500); + return response()->json(['message' => 'Failed to fetch Hetzner images.'], 500); } } @@ -450,7 +450,7 @@ public function sshKeys(Request $request) return response()->json($sshKeys); } catch (\Throwable $e) { - return response()->json(['message' => 'Failed to fetch SSH keys: '.$e->getMessage()], 500); + return response()->json(['message' => 'Failed to fetch Hetzner SSH keys.'], 500); } } @@ -733,7 +733,7 @@ public function createServer(Request $request) return $response; } catch (\Throwable $e) { - return response()->json(['message' => 'Failed to create server: '.$e->getMessage()], 500); + return response()->json(['message' => 'Failed to create Hetzner server.'], 500); } } } diff --git a/routes/web.php b/routes/web.php index fad3c5d29..997045659 100644 --- a/routes/web.php +++ b/routes/web.php @@ -391,7 +391,7 @@ 'Content-Disposition' => 'attachment; filename="'.basename($filename).'"', ]); } catch (Throwable $e) { - return response()->json(['message' => $e->getMessage()], 500); + return response()->json(['message' => 'Failed to download backup.'], 500); } })->name('download.backup'); diff --git a/tests/Feature/HetznerApiTest.php b/tests/Feature/HetznerApiTest.php index bd316ca49..3e8555b11 100644 --- a/tests/Feature/HetznerApiTest.php +++ b/tests/Feature/HetznerApiTest.php @@ -446,3 +446,74 @@ $response->assertStatus(401); }); }); + +describe('GHSA-m8wx-q63q-3w6c — error responses do not leak exception details', function () { + test('locations endpoint returns generic 500 message on upstream failure', function () { + Http::fake([ + 'https://api.hetzner.cloud/v1/locations*' => Http::response([ + 'error' => ['message' => 'INTERNAL_LEAK_TOKEN_abc /var/secret/path'], + ], 500), + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->getJson('/api/v1/hetzner/locations?cloud_provider_token_id='.$this->hetznerToken->uuid); + + $response->assertStatus(500); + $response->assertExactJson(['message' => 'Failed to fetch Hetzner locations.']); + expect($response->getContent())->not->toContain('INTERNAL_LEAK_TOKEN_abc'); + expect($response->getContent())->not->toContain('/var/secret/path'); + }); + + test('server-types endpoint returns generic 500 message on upstream failure', function () { + Http::fake([ + 'https://api.hetzner.cloud/v1/server_types*' => Http::response([ + 'error' => ['message' => 'INTERNAL_LEAK_TOKEN_abc'], + ], 500), + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->getJson('/api/v1/hetzner/server-types?cloud_provider_token_id='.$this->hetznerToken->uuid); + + $response->assertStatus(500); + $response->assertExactJson(['message' => 'Failed to fetch Hetzner server types.']); + expect($response->getContent())->not->toContain('INTERNAL_LEAK_TOKEN_abc'); + }); + + test('images endpoint returns generic 500 message on upstream failure', function () { + Http::fake([ + 'https://api.hetzner.cloud/v1/images*' => Http::response([ + 'error' => ['message' => 'INTERNAL_LEAK_TOKEN_abc'], + ], 500), + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->getJson('/api/v1/hetzner/images?cloud_provider_token_id='.$this->hetznerToken->uuid); + + $response->assertStatus(500); + $response->assertExactJson(['message' => 'Failed to fetch Hetzner images.']); + expect($response->getContent())->not->toContain('INTERNAL_LEAK_TOKEN_abc'); + }); + + test('ssh-keys endpoint returns generic 500 message on upstream failure', function () { + Http::fake([ + 'https://api.hetzner.cloud/v1/ssh_keys*' => Http::response([ + 'error' => ['message' => 'INTERNAL_LEAK_TOKEN_abc'], + ], 500), + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->getJson('/api/v1/hetzner/ssh-keys?cloud_provider_token_id='.$this->hetznerToken->uuid); + + $response->assertStatus(500); + $response->assertExactJson(['message' => 'Failed to fetch Hetzner SSH keys.']); + expect($response->getContent())->not->toContain('INTERNAL_LEAK_TOKEN_abc'); + }); +});