From 1efa15851cae4c43d9169b9af0750c424d8ba6c2 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 3 Sep 2026 21:56:00 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL/HIGH]=20Fix=20missing=20API=20security=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added HTTP security headers via Hono's `secureHeaders` middleware to the runtime application boundary (`server/runtime-app.mjs`) to mitigate mime-sniffing and clickjacking vulnerabilities without coupling security policies to the canonical business application (`server/app.mjs`). Expanded API test coverage to assert the presence of these headers. --- .jules/sentinel.md | 4 ++++ package.json | 2 +- server/runtime-app.mjs | 11 +++++++++++ server/server.mjs | 4 ++-- tests/api/runtime-headers.test.mjs | 17 +++++++++++++++++ 5 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 server/runtime-app.mjs create mode 100644 tests/api/runtime-headers.test.mjs diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 17f338fe..0f09d639 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -128,3 +128,7 @@ **Vulnerability:** The backend CSV export for audit logs neutralized `=`, `+`, `-`, and `@` but failed to neutralize `|` (pipe) characters, allowing potential DDE (Dynamic Data Exchange) injection if exported logs were opened in spreadsheet software. **Learning:** Spreadsheet formula defenses must cover all command-style prefixes including `|` across all CSV export boundaries, both frontend and backend. **Prevention:** Update the sanitization regex in the backend export function to `/^[=+\-@|]/` so that all potentially executable spreadsheet payloads are prefixed with a single quote. +## 2026-09-03 - Added security headers via Hono secureHeaders +**Vulnerability:** The application was missing critical security headers like X-Content-Type-Options, X-Frame-Options, and Strict-Transport-Security, making it vulnerable to mime-sniffing, clickjacking, and man-in-the-middle attacks. +**Learning:** Security-sensitive response header policies must be implemented in the runtime wrapper (`server/runtime-app.mjs`) to keep the canonical application (`server/app.mjs`) agnostic and pass the repository's API security header tests. +**Prevention:** Always implement HTTP security headers using a dedicated middleware (e.g., Hono's `secureHeaders`) applied globally at the outermost runtime layer rather than cluttering business logic routes. diff --git a/package.json b/package.json index 8cefdc74..2a9ab7ac 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "check:python-docstrings": "node scripts/ci/static_coverage_evidence.mjs docstrings", "coverage": "npm run test:coverage", "server": "node server/server.mjs", - "test:api": "node tests/api/auth-secret.test.mjs && node tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs && node tests/api/orchestrator-attribution.test.mjs", + "test:api": "node tests/api/auth-secret.test.mjs && node tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs && node tests/api/orchestrator-attribution.test.mjs && node tests/api/runtime-headers.test.mjs", "test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/toast-accessibility.test.mjs", "test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/auth.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases", "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api", diff --git a/server/runtime-app.mjs b/server/runtime-app.mjs new file mode 100644 index 00000000..007c9283 --- /dev/null +++ b/server/runtime-app.mjs @@ -0,0 +1,11 @@ +import { Hono } from 'hono'; +import { secureHeaders } from 'hono/secure-headers'; +import { app } from './app.mjs'; + +export const runtimeApp = new Hono(); + +// Apply security headers to all routes +runtimeApp.use('*', secureHeaders()); + +// Mount the canonical application +runtimeApp.route('/', app); diff --git a/server/server.mjs b/server/server.mjs index c84c2e25..ad80baf6 100644 --- a/server/server.mjs +++ b/server/server.mjs @@ -1,7 +1,7 @@ import { serve } from '@hono/node-server'; -import { app } from './app.mjs'; +import { runtimeApp } from './runtime-app.mjs'; const port = Number(process.env.PORT) || 8787; -serve({ fetch: app.fetch, port }, (info) => { +serve({ fetch: runtimeApp.fetch, port }, (info) => { console.log(`ScopeWeave API listening on http://localhost:${info.port}`); }); diff --git a/tests/api/runtime-headers.test.mjs b/tests/api/runtime-headers.test.mjs new file mode 100644 index 00000000..ed55b391 --- /dev/null +++ b/tests/api/runtime-headers.test.mjs @@ -0,0 +1,17 @@ +import assert from 'node:assert'; +import { runtimeApp } from '../../server/runtime-app.mjs'; + +async function runTests() { + const res = await runtimeApp.request('/api/health'); + + assert.equal(res.headers.get('x-content-type-options'), 'nosniff'); + assert.equal(res.headers.get('x-frame-options'), 'SAMEORIGIN'); + assert.ok(res.headers.get('strict-transport-security')); + + console.log('✓ API security header tests passed'); +} + +runTests().catch(err => { + console.error(err); + process.exit(1); +}); From 7fd402e535ae754750dd39fa898ac98d3ffaa9ac Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 3 Sep 2026 22:22:28 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL/HIGH]=20Fix=20missing=20API=20security=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added HTTP security headers via Hono's `secureHeaders` middleware to the runtime application boundary (`server/runtime-app.mjs`) to mitigate mime-sniffing and clickjacking vulnerabilities without coupling security policies to the canonical business application (`server/app.mjs`). Expanded API test coverage to assert the presence of these headers. --- .jules/sentinel.md | 4 ++-- tests/api/runtime-headers.test.mjs | 32 +++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 0f09d639..b4a27bde 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -129,6 +129,6 @@ **Learning:** Spreadsheet formula defenses must cover all command-style prefixes including `|` across all CSV export boundaries, both frontend and backend. **Prevention:** Update the sanitization regex in the backend export function to `/^[=+\-@|]/` so that all potentially executable spreadsheet payloads are prefixed with a single quote. ## 2026-09-03 - Added security headers via Hono secureHeaders -**Vulnerability:** The application was missing critical security headers like X-Content-Type-Options, X-Frame-Options, and Strict-Transport-Security, making it vulnerable to mime-sniffing, clickjacking, and man-in-the-middle attacks. -**Learning:** Security-sensitive response header policies must be implemented in the runtime wrapper (`server/runtime-app.mjs`) to keep the canonical application (`server/app.mjs`) agnostic and pass the repository's API security header tests. +**Vulnerability:** The application was missing critical security headers like X-Content-Type-Options, X-Frame-Options, and Strict-Transport-Security, a scanner flagged the lack of headers as a HIGH exploitability risk. However, actual exploitability is bounded if the edge/gateway already enforces HSTS. The runtime header adds defense-in-depth. +**Learning:** Security-sensitive response header policies must be implemented in the runtime wrapper (`server/runtime-app.mjs`) to keep the canonical application (`server/app.mjs`) agnostic and pass the repository.s API security header tests. Also, ScopeWeave runtime and released edge/gateway form a contract where edge may be the canonical owner; if edge is stricter, leaf should not override it. **Prevention:** Always implement HTTP security headers using a dedicated middleware (e.g., Hono's `secureHeaders`) applied globally at the outermost runtime layer rather than cluttering business logic routes. diff --git a/tests/api/runtime-headers.test.mjs b/tests/api/runtime-headers.test.mjs index ed55b391..9366ca7b 100644 --- a/tests/api/runtime-headers.test.mjs +++ b/tests/api/runtime-headers.test.mjs @@ -1,14 +1,36 @@ import assert from 'node:assert'; import { runtimeApp } from '../../server/runtime-app.mjs'; +import { app } from '../../server/app.mjs'; + +async function verifyHeaders(res, path, expectedStatus) { + assert.equal(res.status, expectedStatus, `Expected status ${expectedStatus} for ${path}`); + assert.equal(res.headers.get('x-content-type-options'), 'nosniff', `Missing/incorrect nosniff on ${path}`); + assert.equal(res.headers.get('x-frame-options'), 'SAMEORIGIN', `Missing/incorrect SAMEORIGIN on ${path}`); + // Exact configured HSTS value expected from Hono's default secureHeaders + assert.equal(res.headers.get('strict-transport-security'), 'max-age=15552000; includeSubDomains', `Missing/incorrect exact HSTS on ${path}`); +} async function runTests() { - const res = await runtimeApp.request('/api/health'); + // Test behavior parity before/after header application on domain/body/status + const appRes = await app.request('/api/health'); + const runtimeRes = await runtimeApp.request('/api/health'); + + assert.equal(runtimeRes.status, appRes.status, 'Status behavior differs between app and runtime'); + assert.equal(await runtimeRes.text(), await appRes.text(), 'Body behavior differs between app and runtime'); + + // 1. 2xx OK path + let res = await runtimeApp.request('/api/health'); + await verifyHeaders(res, '/api/health', 200); + + // 2. 401 Unauthorized path (no token) + res = await runtimeApp.request('/api/me'); + await verifyHeaders(res, '/api/me', 401); - assert.equal(res.headers.get('x-content-type-options'), 'nosniff'); - assert.equal(res.headers.get('x-frame-options'), 'SAMEORIGIN'); - assert.ok(res.headers.get('strict-transport-security')); + // 3. 404 Not Found path + res = await runtimeApp.request('/api/does-not-exist'); + await verifyHeaders(res, '/api/does-not-exist', 404); - console.log('✓ API security header tests passed'); + console.log('✓ API security header tests passed (across 200, 401, 404 paths with exact HSTS)'); } runTests().catch(err => {