diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 721687c..6794916 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -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. diff --git a/server/utils/http.test.ts b/server/utils/http.test.ts new file mode 100644 index 0000000..80c6df9 --- /dev/null +++ b/server/utils/http.test.ts @@ -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:;"); + }); +}); diff --git a/server/utils/http.ts b/server/utils/http.ts index 73cf010..70d0107 100644 --- a/server/utils/http.ts +++ b/server/utils/http.ts @@ -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; }