Skip to content

Fix timing leak, CORS enforcement, CI race condition, and frontend hardcoding - #84

Open
potemkin666 with Copilot wants to merge 10 commits into
mainfrom
copilot/refresh-feed-randomize-websites
Open

Fix timing leak, CORS enforcement, CI race condition, and frontend hardcoding#84
potemkin666 with Copilot wants to merge 10 commits into
mainfrom
copilot/refresh-feed-randomize-websites

Conversation

Copilot AI commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Multiple security, correctness, and maintainability issues across API routes, CI workflow, and frontend configuration.

Security

  • Timing-safe token verification (admin-session.js): verifySignedToken did a bare Buffer.length comparison before timingSafeEqual, leaking token format information via timing. Now pads both buffers to equal length before the constant-time comparison:

    const maxLen = Math.max(expectedBuffer.length, receivedBuffer.length);
    const paddedExpected = Buffer.alloc(maxLen);
    const paddedReceived = Buffer.alloc(maxLen);
    expectedBuffer.copy(paddedExpected);
    receivedBuffer.copy(paddedReceived);
    if (!crypto.timingSafeEqual(paddedExpected, paddedReceived) || expectedBuffer.length !== receivedBuffer.length) return null;
  • CORS enforcement (all 7 API routes): applyCorsHeaders() returned a boolean but every caller discarded it. All routes now gate on the return value and early-return 403 for disallowed origins. The function itself now returns false for disallowed origins on all HTTP methods, not just OPTIONS.

CI

  • Merge conflict race condition (update-live-feed.yml): Replaced per-file git checkout --theirs (silently discards the other run's data) with git rebase --abortgit merge -X oursgit push --force-with-lease.

Frontend

  • Centralized API base URL: Extracted hardcoded brialertbackend.vercel.app from 5 files into shared/api-base.mjs. Build script reads BRIALERT_API_BASE env var with fallback.

Performance & correctness

  • countryCodeFor() Map allocation (map-watch.mjs): 70-entry Map was recreated on every call during cluster rendering. Hoisted to module-level COUNTRY_CODE_MAP.
  • lastBrowserPollAt on failure (feed-controller.mjs): Was set unconditionally, making poll timing data misleading after errors. Now only set on success.
  • Stemming minimum length (fusion.mjs): Suffix removal for -ing, -ed, -es, -s could produce 1–2 char stems. Tightened length guards so stems are always ≥ 3 chars.

Cleanup

  • Renamed sourceMayAutoCooldown()getAutoCooldownStatus() (returns object/null, not boolean)
  • Enhanced ESLint config: added prefer-const, no-unused-vars (with ^_ ignore patterns), test-specific override. Fixed 5 pre-existing unused-var issues surfaced by the new rules.

Copilot AI and others added 9 commits April 15, 2026 22:30
…efresh

- Randomize rankedScheduledSources tie-breaking with Math.random() jitter
  instead of stable array index, so same-priority sources are shuffled each run
- Randomize selectHtmlSourcesForRun rotateTier with jitter-based tie-breaking
  instead of deterministic offset rotation within same weight buckets
- Randomize continuationCandidates tie-breaking with jitter so backup sources
  are also picked in varying order

Priority ordering is preserved (incidents > trusted > general), but sources
at the same priority level are shuffled randomly each run to keep the feed fresh.

Agent-Logs-Url: https://github.com/potemkin666/Brialert/sessions/0e50b7ca-6e9d-4b18-9f43-44f8eaeaac44

Co-authored-by: potemkin666 <183807833+potemkin666@users.noreply.github.com>
…ts, confidence bounds

- Escape entry.source and entry.sourceUrl in HTML templates (alert-view-model.mjs)
- Remove file paths from parseJsonFile error messages (github-persistence.js)
- Sanitize workflowMessage in approve-source.js and restore-source.js
- Remove raw GitHub API error forwarding in trigger-live-feed.js
- Deduplicate source-processing logic: main loop now calls attemptSourceBuild()
- Add missing Playwright constant exports to config.mjs
- Add 0-1 bounds check on confidenceScore (alert-view-model.mjs)

Agent-Logs-Url: https://github.com/potemkin666/Brialert/sessions/125f5bed-88b1-4f52-9efa-960df9dc4e41

Co-authored-by: potemkin666 <183807833+potemkin666@users.noreply.github.com>
…undant parses

Agent-Logs-Url: https://github.com/potemkin666/Brialert/sessions/42b1a3c3-155f-47e7-aa93-203dba553b5b

Co-authored-by: potemkin666 <183807833+potemkin666@users.noreply.github.com>
…ve !important, rename duplicate clean()

Agent-Logs-Url: https://github.com/potemkin666/Brialert/sessions/6ba44832-573e-445c-b4f5-c0e7c05c9c0b

Co-authored-by: potemkin666 <183807833+potemkin666@users.noreply.github.com>
…ct 404 inline JS

Agent-Logs-Url: https://github.com/potemkin666/Brialert/sessions/192e4b0f-d0f3-4fea-890d-f58fbf3a9011

Co-authored-by: potemkin666 <183807833+potemkin666@users.noreply.github.com>
…centralize API base URL

Agent-Logs-Url: https://github.com/potemkin666/Brialert/sessions/fc5b791e-3667-44c5-998c-f7f1e5868414

Co-authored-by: potemkin666 <183807833+potemkin666@users.noreply.github.com>
…gthen stemming, rename sourceMayAutoCooldown, enhance ESLint

Agent-Logs-Url: https://github.com/potemkin666/Brialert/sessions/c351d7ab-8cd8-44cb-af8a-33576acccd28

Co-authored-by: potemkin666 <183807833+potemkin666@users.noreply.github.com>
@potemkin666

Copy link
Copy Markdown
Owner

@copilot resolve the merge conflicts in this pull request

@potemkin666
potemkin666 marked this pull request as ready for review April 15, 2026 23:49
Copilot AI review requested due to automatic review settings April 15, 2026 23:49
…randomize-websites

# Conflicts:
#	data/brialert.sqlite

Co-authored-by: potemkin666 <183807833+potemkin666@users.noreply.github.com>

Copilot AI commented Apr 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Merge conflicts resolved in b98718a. The only conflict was data/brialert.sqlite (modify/delete) — kept the deletion since our branch intentionally removed it from git tracking (it's in .gitignore). All 115 tests pass and lint is clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses several security and correctness issues across the API layer, build scripts/CI workflow, and frontend by tightening request validation/enforcement, reducing hardcoded configuration, and improving runtime behavior/performance.

Changes:

  • Hardened security controls (timing-safe token verification, stricter CORS gating in API routes, SSRF hostname blocking for URL validation, improved XSS escaping in some UI rendering).
  • Centralized/configured backend base URL usage across frontend modules and build outputs; improved accessibility markup in index.html.
  • Build/ops improvements (CI conflict handling strategy, reduced per-call allocations/caching, refactors to reduce duplication and improve run stats).

Reviewed changes

Copilot reviewed 30 out of 32 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/modal-remote-client.test.mjs Updates test to assert URL built from centralized DEFAULT_API_BASE.
styles.css Removes !important from briefing-mode display rules.
shared/map-watch.mjs Hoists country code Map to module scope to avoid repeated allocations.
shared/fusion.mjs Tightens stemming guards to avoid overly-short stems.
shared/feed-controller.mjs Only updates lastBrowserPollAt on successful fetch.
shared/api-base.mjs Adds centralized DEFAULT_API_BASE constant for client-side API calls.
shared/alert-view-model.mjs Escapes some rendered fields and clamps confidenceScore to [0,1].
scripts/build-live-feed/geo.mjs Caches geo regex compilation for performance.
scripts/build-live-feed/config.mjs Exposes additional Playwright/config constants via env-backed helpers.
scripts/build-live-feed.mjs Refactors source build logic into attemptSourceBuild, adds jitter-based ordering, and uses env-configured API base in generated quarantine HTML.
index.html Adds CSP reminder comment, improves form accessibility via sr-only labels, and defers Leaflet script.
eslint.config.mjs Enables prefer-const and no-unused-vars (with test override).
app/render/modal-ui-controller.mjs Removes unused import.
app/render/modal-remote-client.mjs Uses centralized DEFAULT_API_BASE for remote brief endpoint.
app/render/live.mjs Avoids unused parameter lint error via _modalController.
app/redirect.js Extracts redirect logic into a shared script.
app/feed/source-requests.mjs Uses centralized API base; refines rate-limiting behavior; renames trim helper.
app/feed/index.mjs Uses centralized API base for trigger endpoints.
app/boot/index.mjs Removes unused import.
api/trigger-live-feed.js Enforces CORS allowlist return value; addresses trigger timing/race behavior; tightens error messaging.
api/restore-source.js Enforces CORS allowlist return value; avoids leaking internal workflow errors.
api/request-source.js Enforces CORS allowlist return value.
api/quarantined-sources.js Enforces CORS allowlist return value.
api/auth/session.js Enforces CORS allowlist return value.
api/auth/logout.js Enforces CORS allowlist return value.
api/approve-source.js Enforces CORS allowlist return value; avoids leaking internal workflow errors.
api/_lib/github-persistence.js Removes internal JSON parse detail leakage; adds SSRF-oriented hostname blocking to URL validation.
api/_lib/admin-session.js Fixes timing leak in signature check; changes CORS helper to return allow/deny for all methods.
404.html Switches inline redirect script to app/redirect.js.
.gitignore Ignores SQLite files under data/.
.github/workflows/update-live-feed.yml Adjusts conflict handling to abort rebase and merge with -X ours, with force-with-lease push.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread 404.html
Comment on lines 7 to +8
<meta http-equiv="refresh" content="0; url=/index.html">
<script>
const canonicalTarget = location.pathname.toLowerCase().includes('/brialert')
? '/Brialert/index.html'
: '/index.html';
const query = location.search || '';
const hash = location.hash || '';
if (location.pathname.toLowerCase() !== canonicalTarget.toLowerCase()) {
location.replace(canonicalTarget + query + hash);
}
</script>
<script src="app/redirect.js"></script>

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meta http-equiv="refresh" content="0; url=/index.html" will likely fire before app/redirect.js finishes downloading/executing, so requests under /Brialert/... can be redirected to the wrong canonical target on slower connections. Prefer making the JS redirect the primary mechanism (remove the 0s meta refresh or delay it), and optionally keep a <noscript> fallback to /index.html.

Copilot uses AI. Check for mistakes.
<div id="toast" class="toast" aria-live="polite"></div>
<script>
const DEFAULT_API_BASE = 'https://brialertbackend.vercel.app';
const DEFAULT_API_BASE = '${process.env.BRIALERT_API_BASE || 'https://brialertbackend.vercel.app'}';

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interpolates process.env.BRIALERT_API_BASE directly into a single-quoted JS string inside generated HTML. If the env var contains ', newlines, or </script>, it can break the page or inject script. Serialize the value safely (e.g., use JSON.stringify(...) for the string literal) before embedding it into the <script> block.

Suggested change
const DEFAULT_API_BASE = '${process.env.BRIALERT_API_BASE || 'https://brialertbackend.vercel.app'}';
const DEFAULT_API_BASE = ${JSON.stringify(process.env.BRIALERT_API_BASE || 'https://brialertbackend.vercel.app').replace(/</g, '\\u003c')};

Copilot uses AI. Check for mistakes.
return `<div class="corroboration-list">${sources.map((entry) => `
<article class="corroboration-item">
<a href="${entry.sourceUrl}" target="_blank" rel="noreferrer">${entry.source}</a>
<a href="${escapeHtml(entry.sourceUrl)}" target="_blank" rel="noreferrer">${escapeHtml(entry.source)}</a>

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escaping entry.sourceUrl prevents HTML injection, but it doesn't prevent dangerous URL schemes like javascript: or data:. Since sourceUrl ultimately comes from feeds (item.link), consider normalising/validating to only allow http:/https: (otherwise render as # or omit the link).

Copilot uses AI. Check for mistakes.
Comment thread api/_lib/admin-session.js
Comment on lines 150 to +161
export function applyCorsHeaders(request, response, methods) {
const requestOrigin = normaliseOrigin(request?.headers?.origin);
const allowedOrigins = new Set(getAllowedOrigins());
if (requestOrigin && allowedOrigins.has(requestOrigin)) {
const originAllowed = !requestOrigin || allowedOrigins.has(requestOrigin);
if (requestOrigin && originAllowed) {
response.setHeader('Access-Control-Allow-Origin', requestOrigin);
response.setHeader('Access-Control-Allow-Credentials', 'true');
response.setHeader('Vary', 'Origin');
}
response.setHeader('Access-Control-Allow-Methods', methods);
response.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (request?.method === 'OPTIONS') {
return !requestOrigin || allowedOrigins.has(requestOrigin);
}
return true;
return originAllowed;

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normaliseOrigin() returns '' for values like Origin: null (opaque origins) or other unparsable Origin headers. With the new “deny when applyCorsHeaders() returns false” gating, those requests will now be treated as having no origin and be allowed. Consider explicitly treating Origin: null / invalid Origin headers as disallowed when an Origin header is present, so the allowlist can’t be bypassed via an opaque origin.

Copilot uses AI. Check for mistakes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 29993fc857

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .gitignore
.debug-artifacts/
.DS_Store
Thumbs.db
data/*.sqlite

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop ignoring the SQLite artifact committed by CI

Adding data/*.sqlite to .gitignore makes git add data/brialert.sqlite fail in the commit-live-feed job (.github/workflows/update-live-feed.yml), so scheduled feed updates will error whenever the builder outputs that file. This is a regression from the previous behavior where the SQLite artifact could be staged and committed; now the workflow exits non-zero with "path is ignored" instead of publishing refreshed data.

Useful? React with 👍 / 👎.

Comment thread api/trigger-live-feed.js
// Claim the slot before the async dispatch so concurrent requests see
// the lock immediately (avoids check-then-act race).
const previousTriggerTime = lastTriggerTime;
lastTriggerTime = now;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Roll back trigger lock when dispatch throws

Setting lastTriggerTime = now before dispatch is fine for race prevention, but this path only rolls it back for non-OK HTTP responses; exceptions (e.g., network failure or early config error) leave the lock set and cause subsequent requests to return 429 for 60 seconds even though no workflow was triggered. That blocks legitimate retries after transient failures and makes manual recovery unnecessarily slower.

Useful? React with 👍 / 👎.

Comment thread 404.html
location.replace(canonicalTarget + query + hash);
}
</script>
<script src="app/redirect.js"></script>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a path-safe redirect script reference on 404 page

Switching from inline redirect logic to <script src="app/redirect.js"> introduces a path-resolution bug for deep URLs: on routes like /Brialert/foo/bar, the browser resolves this to /Brialert/foo/app/redirect.js, so the script fails to load and the canonical /Brialert/index.html redirect logic never runs. In that case users fall back to the meta refresh target /index.html, which can send them to the wrong site root.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants