Skip to content

fix(security): resolve all high/critical CodeQL findings (#177, #179, #180)#183

Merged
m-khan-97 merged 4 commits into
devfrom
fix/177-179-180-high-priority-vulns
Jul 13, 2026
Merged

fix(security): resolve all high/critical CodeQL findings (#177, #179, #180)#183
m-khan-97 merged 4 commits into
devfrom
fix/177-179-180-high-priority-vulns

Conversation

@TFT444

@TFT444 TFT444 commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #177, #179, #180 — all high and critical CodeQL findings (34 total across the 4 issues filed from the code-scanning review; this PR covers the 3 grouped as high/critical, per team decision to split high+critical into one PR and medium into a separate one).

  • Fix embed-URL validation bypass — stored XSS risk in blog/event preview (CodeQL: DOM text reinterpreted as HTML + incomplete URL sanitization) #179 (critical — stored XSS): toEmbedUrl() in website/script.js validated embed URLs with a substring check (raw.includes('youtube.com/embed')) and returned the unvalidated raw input when it matched — bypassable with a crafted string, breaking out of the iframe's src="..." attribute. Rewritten to validate via new URL(raw).hostname against an exact allowlist. Added website/test_toEmbedUrl.mjs, a dependency-free test that loads the real production file (no duplicated logic) and verifies the exploit string is rejected while legitimate YouTube/Vimeo URLs still work.
  • Fix broken AI settings — restore API key persistence without localStorage (CodeQL: clear-text storage) #180 (high — clear-text storage, also a live bug): frontend/src/utils/aiApi.js's AI provider API key storage. A prior attempt to fix this same CodeQL alert had removed the localStorage.setItem call but left the getter/isConfigured() reading from the now-permanently-empty key — silently breaking all four AI features regardless of what key a user entered. Moved the key to a module-scoped in-memory variable: satisfies CodeQL (never touches browser storage) and restores the feature. Added frontend/src/utils/aiApi.test.mjs.
  • Sanitize API error responses — stop leaking exception details (CodeQL: information exposure) #177 (medium-severity CodeQL, bundled here per the high+critical grouping): 27 findings across api/app.py and 8 route modules under api/routes/ where exceptions were reflected back to the client via str(exc) or an f-string, including from broad except Exception blocks that could surface raw DB driver errors. Replaced with fixed, generic messages; existing/added logger.error(...) calls preserve full detail server-side. Extracted a shared _ai_error_response() helper in ai.py for 4 near-identical handlers. Also fixed an f-string leak in compliance.py's FileNotFoundError handler that a plain "detail" key search would have missed.

Self-review

Ran a high-effort, 6-agent parallel review against the full diff. One real issue found in my own work: escapeHTML() had been added at 3 interpolation sites in website/script.js that turned out to be .textContent assignments, not .innerHTML.textContent never interprets HTML, so escaping there added no security value while risking visible text corruption (a literal & on screen) if the underlying value ever contained a special character. Reverted all 3; toEmbedUrl()'s URL-origin validation is the real, load-bearing fix and doesn't depend on them.

Everything else the review surfaced was low-severity/architectural (no shared error-response helper across all 9 API files yet, no repo-wide lint rule preventing a future str(exc) regression, two new JS test files each hand-roll similar sandboxing boilerplate since neither website/ nor frontend/ has an existing test framework) — noted as follow-up-worthy but not blocking this fix.

Test plan

  • ruff check / ruff format --check — clean
  • Full pytest suite: 348 passed, same 2 pre-existing unrelated local failures, no regressions
  • 14 new regression tests (tests/test_error_exposure.py, tests/test_ai_error_exposure.py) asserting error responses never contain a "detail" key or the raw exception text, across every route touched
  • website/test_toEmbedUrl.mjs (13 cases) and frontend/src/utils/aiApi.test.mjs (6 cases) — both load and test the real production source, wired into CI (ci.yml: new website job, new step in the frontend job)
  • No merge conflicts against dev

TFT444 added 4 commits July 13, 2026 12:18
toEmbedUrl()'s fallback branch used raw.includes(...) to check for a
known-safe embed URL, then returned the unvalidated input unmodified.
A crafted string containing the substring anywhere could pass the check
while breaking out of the iframe's src="..." attribute it gets
interpolated into unescaped, injecting arbitrary markup.

Validate via new URL(raw).hostname against an exact allowlist instead,
and escape the result at every interpolation site as defense in depth.
Also escape the two playground select-field values that were
concatenated into an HTML-shaped string unescaped.

Fixes #179 (CodeQL: DOM text reinterpreted as HTML x3, incomplete URL
substring sanitization x1).
A prior fix for this same CodeQL alert removed the localStorage.setItem
call for the user's bring-your-own AI provider key but left the
getter/isConfigured() reading from the now-permanently-empty key,
silently breaking every AI feature (chat, summary, insights,
prioritisation all gated behind isConfigured()).

Store the key in a module-scoped variable instead of any browser
storage. It never survives a page reload (the standard, expected
trade-off for a secret that must not persist), but the feature works
again and the key is never written to localStorage/sessionStorage.
provider/model (not secret) still persist in localStorage as before.

Fixes #180 (CodeQL: clear text storage of sensitive information).
Every affected route returned str(exc) (or an f-string embedding it)
directly in the JSON response body. Some catch broad `except Exception`,
which can surface raw database driver errors, connection details, or
internal paths to the client rather than just intentional validation
messages.

Replace with a fixed, generic message per error class; the existing
logger.error(...)/logger.warning(...) calls already capture full detail
server-side. In api/routes/ai.py, extract a shared _ai_error_response()
helper since the same 3-exception pattern (VectorStoreNotBuilt,
ValueError, RuntimeError from get_completion()) repeated verbatim across
4 routes, and add the server-side logging that was missing there
entirely. Also fixes an f-string leak in compliance.py's
FileNotFoundError handler that the "detail" key search missed.

Fixes #177 (CodeQL: information exposure through an exception, all 27
findings across api/app.py and 8 api/routes/ modules).
Self-review caught that the defense-in-depth escapeHTML() calls added
for #179 wrapped values assigned via .textContent, not .innerHTML.
.textContent never interprets HTML, so escaping there adds no security
value while risking visible text corruption (a literal "&" shown
on screen) if the underlying value ever contains a special character.

toEmbedUrl()'s URL-origin validation is the real, load-bearing fix and
is unaffected by this change — it guarantees embedUrl can only ever be
'' or a well-formed https://youtube.com|vimeo.com URL regardless of
which DOM property ultimately consumes it.
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@TFT444 TFT444 self-assigned this Jul 13, 2026
@TFT444
TFT444 requested review from m-khan-97 and removed request for SHAURYAKSHARMA24, parthrohit22 and vogonPrayas July 13, 2026 12:26

@m-khan-97 m-khan-97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thorough review of all three fixes here, checked against actual source rather than just the description:

#179 (stored XSS) — verified the new `toEmbedUrl()` logic directly: `new URL(raw).hostname` against an exact-match allowlist correctly defeats every bypass class I'd try — substring tricks (`evil.com/?x=youtube.com/embed`), subdomain spoofing (`youtube.com.evil.com`), and userinfo tricks (WHATWG URL parsing puts anything before `@` into userinfo, not hostname, so that vector is closed by construction, not just by the allowlist). The test file backs this with the exact bypass strings you'd expect an attacker to try, including markup injection. Good fix.

#180 (clear-text storage + the live bug it caused) — really glad this called out that the previous attempted fix had already broken all four AI features by removing the localStorage write but leaving the getter reading from it. Moving to a module-scoped in-memory variable satisfies CodeQL and restores the feature in one change, with tests for both properties (never touches localStorage under any key, and still round-trips correctly in memory). The documented trade-off (key doesn't survive reload) is the right call and clearly explained in the comment.

#177 (error exposure) — the sweep across all 8 route files plus `api/app.py`'s error handlers is complete and consistent. Good catch on `compliance.py`'s f-string leak specifically — that's a real, distinct leak vector from the `"detail"` key pattern the rest of the routes had, and a naive grep for `"detail"` would've missed it. The 25 new regression tests use a distinctive marker string rather than checking for generic wording, which is the right way to make this kind of test actually catch a regression instead of coincidentally passing.

Also appreciated the self-review note about reverting the `.textContent` escaping — recognizing that fix added no security value and could visibly corrupt text is exactly the kind of check that should happen before merge, not after someone notices garbled output in production.

Approving.

@m-khan-97
m-khan-97 merged commit 36a9172 into dev Jul 13, 2026
19 checks passed
@m-khan-97
m-khan-97 deleted the fix/177-179-180-high-priority-vulns branch July 13, 2026 20:42

@ritiksah141 ritiksah141 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two security issues need to be addressed before this PR can be approved.

  1. toEmbedUrl() validates with new URL(raw) but returns the original raw string. An allowed hostname can therefore retain attribute-breaking characters. For example, https://youtube.com/embed/x" onload="alert(1) passes the protocol and hostname checks and is returned unchanged. Return the canonical parsed.href after validating the expected YouTube or Vimeo embed pathname, and add regression cases for quotes or markup characters on an allowed hostname.

  2. The in-memory API key change does not remove a legacy ai_api_key already stored by earlier releases. Existing users can therefore retain the clear-text secret in localStorage indefinitely unless they manually use clear(). Remove the legacy entry during module initialization or a one-time migration, and add a test that pre-populates ai_api_key and verifies it is deleted without being read into memory.

The remaining changes and tests look sound. Local verification passed: 329 tests passed, 3 skipped; the 17 targeted error-exposure tests passed; both JavaScript regression suites passed; Ruff and formatting checks passed.

Vishnu2707 pushed a commit that referenced this pull request Jul 15, 2026
Two residual issues from #183's merge, found in post-merge review:

- toEmbedUrl() validated the host via new URL() but returned the raw
  input, so an attribute-injection payload on an allowed host (e.g.
  https://www.youtube.com/embed/abc" onload="alert(1)) passed validation
  and kept its literal double-quote, breaking out of the iframe src="..."
  attribute. Now returns parsed.href, which percent-encodes quotes,
  spaces, and brackets.
- The switch to in-memory key storage removed the localStorage write but
  never purged keys that older builds had already stored, so existing
  users kept a plaintext ai_api_key indefinitely. Added a one-time purge
  on module load (not read into memory) and in clear().

Both fixes ship with regression tests confirmed to fail against the
pre-fix code and pass against the fix.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

Sanitize API error responses — stop leaking exception details (CodeQL: information exposure)

3 participants