fix(security): never vend the LLM API key to the browser - #286
Open
NovakPAai wants to merge 1 commit into
Open
Conversation
GET /api/llm-config returned the stored API key verbatim on every
Settings load — it sat unmasked in each network response/devtools log
and the frontend dropped it straight into the password input's value.
The GitHub endpoints already follow the right rule ("Never vend raw
tokens to the browser", /api/github/profile strips token/repoToken);
the LLM endpoint was the one exception.
- GET /api/llm-config now returns { model, url, hasKey, keyHint } —
keyHint is a ••••-masked last-4 so the user can tell which key is
stored without the secret ever reaching the DOM.
- Because the key no longer round-trips, POST with an empty apiKey
field means "keep the stored key" (otherwise every URL/model tweak
would silently wipe it); an explicit { clearApiKey: true } removes it.
- saveLLMConfig writes ~/.claude/codedash-llm.json with mode 0600 and
chmods a pre-existing world-readable file from older versions.
- Frontend shows the stored-key state via the input placeholder
("••••1234 (saved — type to replace)"), never pre-fills the value;
autocomplete="new-password" keeps browser autofill managers out.
saveLLMSettings now also surfaces save failures instead of toasting
"saved" unconditionally.
- leaderboard.js: rel="noopener noreferrer" on all target=_blank links
(reverse-tabnabbing).
Verified live: GET carries no apiKey field, hint shows ••••1234, file
lands -rw-------, an empty-key save preserves the key, clearApiKey
clears it. Source-contract tests added (test/llm-config-redaction.test.js).
5 tasks
vakovalskii
added a commit
that referenced
this pull request
Aug 3, 2026
Conflict in leaderboard.js: #288 replaces the inline GitHub device-code modal markup with _lbBuildAuthModal() (focus trap + Escape + focus return). Took #288 for that hunk only — resolving the whole file to either side would have dropped the other PR's work; #286's rel="noopener noreferrer" on the remaining three target="_blank" links is kept (its test asserts all four).
vakovalskii
added a commit
that referenced
this pull request
Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A server-side audit found that
GET /api/llm-configreturned the stored LLM API key (Settings → Integrations → AI titles) verbatim on every Settings load — unmasked in each network response and devtools log, then pre-filled into the password input's value. The GitHub endpoints already follow the right rule (/api/github/profileexplicitly stripstoken/repoTokenwith a "Never vend raw tokens to the browser" comment); the LLM endpoint was the one exception.GET /api/llm-confignow returns{ model, url, hasKey, keyHint }—keyHintis a••••-masked last-4 so the user can tell which key is stored without the secret ever reaching the DOM.POSTwith an emptyapiKeyfield means "keep the stored key" (otherwise every URL/model tweak would silently wipe it); an explicit{ clearApiKey: true }removes it.saveLLMConfigwrites~/.claude/codedash-llm.jsonwith mode0600andchmods a pre-existing world-readable file from older versions (matching the 0600 convention the workspace-layouts/commands stores already use).••••1234 (saved — type to replace)), never pre-fills the value;autocomplete="new-password"keeps browser autofill managers out.saveLLMSettingsalso surfaces save failures now instead of toasting "saved" unconditionally.rel="noopener noreferrer"on alltarget="_blank"links inleaderboard.js(reverse-tabnabbing).Test plan
node --test "test/**/*.test.js"— 262 passed, 1 skipped (win32-only); includes 5 new source-contract tests (test/llm-config-redaction.test.js)apiKeyfield and shows••••1234; config file lands-rw-------; a save with an empty key field preserves the stored key;clearApiKey: trueclears it (original local config backed up and restored after the test)🤖 Generated with Claude Code