Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@
**Learning:** Unbounded network requests tie up valuable system resources while waiting for a response that may never arrive.
**Prevention:** When implementing external API requests using `fetch` or similar HTTP clients, always enforce a timeout (e.g., using `AbortSignal.timeout(5000)`) to prevent unbounded waiting and application unresponsiveness.

* **Information Disclosure in API Errors**: When returning errors from the backend to the client, avoid passing the raw `.message` property of system or internal errors for 500-level statuses. Instead, sanitize 500-level errors by substituting a generic fallback message like `"Internal Server Error"` to prevent exposing sensitive internal state (e.g., file paths, IPs, connection strings, API keys) which might be contained in the raw error message.
## 2026-07-04 - Missing Content-Security-Policy
**Vulnerability:** The application was missing a `Content-Security-Policy` header, which is a critical defense against XSS and injection attacks.
**Learning:** A CSP provides defense in depth. Even if XSS vectors exist, a strong CSP restricts what an attacker can do (e.g., executing arbitrary scripts, loading malicious frames).
**Prevention:** Apply a robust baseline `Content-Security-Policy` in `server/utils/http.ts`. Ensure to include directives like `base-uri 'self'` to prevent base tag injection attacks that hijack relative URLs. Note: in this app `unsafe-eval` is kept for `d3-dsv` parsing.
14 changes: 14 additions & 0 deletions server/utils/http.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, it, expect } from 'bun:test';
import { addSecurityHeaders } from './http';

describe('addSecurityHeaders', () => {
it('adds security headers to the response', () => {
const response = new Response('ok');
const secureResponse = addSecurityHeaders(response);

expect(secureResponse.headers.get('X-Content-Type-Options')).toBe('nosniff');
expect(secureResponse.headers.get('X-Frame-Options')).toBe('DENY');
expect(secureResponse.headers.get('Strict-Transport-Security')).toBe('max-age=31536000; includeSubDomains');
expect(secureResponse.headers.get('Content-Security-Policy')).toBe("default-src 'self'; base-uri 'self'; script-src 'self' 'sha256-mhDq8RP/TAuNwiFSk7hwZsZ3tIWH410AupSuJ9xEhZg=' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https: wss:;");
});
});
1 change: 1 addition & 0 deletions server/utils/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,6 @@ export function addSecurityHeaders(response: Response): Response {
response.headers.set('X-Frame-Options', 'DENY');
// X-XSS-Protection is deprecated; use CSP instead
response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
response.headers.set('Content-Security-Policy', "default-src 'self'; base-uri 'self'; script-src 'self' 'sha256-mhDq8RP/TAuNwiFSk7hwZsZ3tIWH410AupSuJ9xEhZg=' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https: wss:;");
return response;
}
Loading