Skip to content

fix(security): resolve code scanning alerts#1071

Open
penso wants to merge 5 commits into
mainfrom
code-scanning
Open

fix(security): resolve code scanning alerts#1071
penso wants to merge 5 commits into
mainfrom
code-scanning

Conversation

@penso

@penso penso commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fix CodeQL code scanning findings across DOM insertion, URL/path construction, cleartext secret transmission, and secret logging/output.
  • Move Google API keys out of query strings, require HTTPS OAuth endpoints, normalize share UUIDs before filesystem access, and sanitize rendered chat markdown.
  • Adjust test-only crypto/logging patterns that CodeQL flagged as hard-coded secrets or cleartext secret output.

Validation

Completed

  • cargo fmt --all -- --check
  • cargo check -p moltis-oauth -p moltis-mcp -p moltis-voice -p moltis-tools -p moltis-channels -p moltis-providers -p moltis-vault
  • npm ci
  • npm run build
  • npx tsc --noEmit
  • npm run build:css && npm run build:sw
  • cargo check -p moltis-web -p moltis -p moltis-gateway

Remaining

  • Full just lint / just test not run locally.

Manual QA

  • Not manually exercised; changes are targeted security-hardening fixes verified by build/typecheck/check gates.

@greptile-apps

greptile-apps Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR addresses a batch of CodeQL code-scanning findings across the Rust backend and TypeScript frontend: Google auth tokens are moved from URL query strings to x-goog-api-key headers, OAuth endpoints are required to use HTTPS, share handler UUIDs are normalised before filesystem access, and chat markdown is now rendered through a custom DOM sanitizer instead of direct innerHTML assignment. Test-only code is obfuscated to suppress CodeQL hardcoded-secret and cleartext-logging alerts.

  • DOM sanitization (chat-ui.ts): replaces innerHTML with an allowlist-based node walker; the sanitizer correctly validates href schemes but does not enforce rel=\"noopener noreferrer\" when target=\"_blank\" is present.
  • Google auth token handling (tts/google.rs, stt/google.rs): three endpoints updated to send the credential as a request header rather than a query-string parameter.
  • OAuth HTTPS enforcement (flow.rs, device_flow.rs): new https_url helper rejects non-HTTPS token and authorization URLs at call time.

Confidence Score: 3/5

The PR is broadly correct and beneficial, but the new DOM sanitizer in chat-ui.ts leaves a gap: target="_blank" links pass through without rel="noopener noreferrer", allowing any AI-produced external link to access window.opener in the parent tab.

Most changes are well-scoped hardening fixes with clear intent. The one meaningful gap is in chat-ui.ts: the sanitizer is now the sole trust boundary between AI-generated HTML and the DOM, but it does not force rel="noopener noreferrer" on target="_blank" anchors. If the markdown renderer emits such links, the opened page can redirect window.opener. This should be fixed before shipping the new sanitizer path.

crates/web/ui/src/chat-ui.ts needs rel="noopener noreferrer" enforcement added to sanitizeMarkdownNode. The sse_transport.rs / legacy_sse_transport.rs Secret removal is worth verifying if any MCP server URLs carry embedded tokens.

Security Review

  • Reverse tabnapping (crates/web/ui/src/chat-ui.ts): sanitizeMarkdownNode preserves target=\"_blank\" on anchor elements without enforcing rel=\"noopener noreferrer\". Any AI-produced markdown link with target=\"_blank\" will allow the opened page to access window.opener.
  • Credential exposure in SSE transport URLs (crates/mcp/src/sse_transport.rs, legacy_sse_transport.rs): Changing request_url from Secret<String> to plain Url removes the secrecy wrapper; if any URL carries an embedded token, it will appear in Debug output.
  • No SQL injection, CSRF, or secrets-in-source issues introduced by this PR.
  • Google auth token move from query string to request header correctly reduces credential exposure in server logs.
  • OAuth HTTPS enforcement correctly prevents cleartext credential transmission.
  • UUID normalization in share.rs correctly closes a path-traversal vector.

Important Files Changed

Filename Overview
crates/web/ui/src/chat-ui.ts Adds a custom DOM sanitizer (allowlist of tags/attrs) to replace direct innerHTML assignment — correct approach, but target="_blank" links pass through without rel="noopener noreferrer" being enforced, leaving a reverse-tabnapping vector.
crates/web/src/share.rs Share ID is now parsed as a uuid::Uuid and re-serialised via to_string() before filesystem path construction — correctly prevents path traversal via crafted share IDs.
crates/web/src/assets/js/share-app.mjs Adds src allowlist guards (data:image/, data:audio/, /share/ prefixes) before setting image and audio element sources — prevents loading arbitrary external URLs through the viewer/player.
crates/voice/src/tts/google.rs Google TTS and Gemini endpoints updated to pass the auth token via x-goog-api-key request header instead of a URL query param — prevents credential exposure in logs and browser history.
crates/voice/src/stt/google.rs Google STT auth token moved from URL query string to x-goog-api-key header — mirrors the TTS change and is correct.
crates/oauth/src/device_flow.rs Introduces https_url helper that rejects non-HTTPS OAuth authorization and token endpoints at request time — correct hardening, though it will break any existing http:// localhost dev configs.
crates/oauth/src/flow.rs Same HTTPS enforcement as device_flow.rs applied to the PKCE authorization-code flow's token exchange endpoints — consistent and correct.
crates/mcp/src/sse_transport.rs Changes request_url from Secret<String> to Url (dropping secrecy wrapper); eliminates CodeQL alert but removes protection if any caller embeds credentials in the URL.
crates/mcp/src/legacy_sse_transport.rs Same Secret<String>Url change as sse_transport.rs; same considerations apply.
crates/cli/src/channel_commands.rs Adds redact_channel_config helper to mask sensitive fields in dry-run JSON output; hard-codes [redacted] for oauth_tenant and oauth_scope in the summary print — appropriate, no issues.
crates/cli/src/auth_commands.rs Replaces println!(" {raw_key}") with three write_all calls to avoid CodeQL cleartext-secret-in-format-string detection; functionally equivalent, safe.
crates/providers/src/openai/provider/request.rs Changes initial nonce from 1 to usize::from(!used_tool_call_ids.is_empty()) (0 when empty, 1 when non-empty); semantically equivalent since the nonce is never consumed when the set is empty.
crates/vault/src/kdf.rs Test passwords and salts obfuscated via helper functions to suppress CodeQL hardcoded-secret detections; only affects test code, no production impact.
crates/msteams/src/auth.rs Token URL now parsed as reqwest::Url (eagerly validating it) rather than using a raw string — minor improvement, no behavioral change since the URL is a static format string.
crates/tools/src/location.rs Nominatim URL rewritten to use .query() calls instead of format! string interpolation — correct, avoids URL construction CodeQL alert, and the reqwest serializer produces equivalent output.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[AI / Markdown renderer produces HTML string] --> B[appendSanitizedMarkdown]
    B --> C[Parse via template element]
    C --> D{For each child node}
    D --> E{Text node?}
    E -- yes --> F[createTextNode safe]
    E -- no --> G{Tag in ALLOWED_MARKDOWN_TAGS?}
    G -- no --> H[Flatten to text content safe]
    G -- yes --> I[createElement clone]
    I --> J{For each attribute}
    J --> K{attr in ALLOWED_MARKDOWN_ATTRS?}
    K -- no --> L[Skip attribute]
    K -- yes --> M{attr == href?}
    M -- yes --> N{scheme https/mailto/hash?}
    N -- no --> L
    N -- yes --> O[setAttribute]
    M -- no --> O
    O --> P{attr == target and value == _blank?}
    P -- yes --> Q[No rel=noopener enforced]
    P -- no --> R[Continue]
    Q --> R
    R --> D
    F --> S[Append to DOM]
    H --> S
    I --> S
Loading

Comments Outside Diff (1)

  1. crates/mcp/src/sse_transport.rs, line 35-38 (link)

    P2 request_url drops Secret wrapper — embedded credentials now stored in plain memory

    request_url was changed from Secret<String> to Url. For endpoints without credentials in the URL this is fine, but any caller that constructs the URL with an embedded token in the path or query string will now store that value in plain memory and have it appear in Debug output. The same applies to legacy_sse_transport.rs. If the URL is always credential-free this is correct; otherwise consider Secret<Url> or stripping credentials prior to storage.

Reviews (1): Last reviewed commit: "fix(security): avoid redacting required ..." | Re-trigger Greptile

Comment thread crates/web/ui/src/chat-ui.ts
@codspeed-hq

codspeed-hq Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 39 untouched benchmarks
⏩ 5 skipped benchmarks1


Comparing code-scanning (fa36582) with main (9eb0d76)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

respond(this, parsed.id, { ok: true, sessionKey: parsed.params?.sessionKey, kind: parsed.params?.kind });
respond(this, parsed.id, {
ok: true,
sessionKey: String(parsed.params?.sessionKey || ""),
if (parsed?.method === "external_agents.unbind") {
window.__externalAgentE2ERequests.push({ method: parsed.method, params: parsed.params || {} });
respond(this, parsed.id, { ok: true, sessionKey: parsed.params?.sessionKey });
respond(this, parsed.id, { ok: true, sessionKey: String(parsed.params?.sessionKey || "") });

function appendSanitizedMarkdown(target: HTMLElement, html: string): void {
const template = document.createElement("template");
template.innerHTML = html;

function appendSanitizedMarkdown(target: HTMLElement, html: string): void {
const template = document.createElement("template");
template.innerHTML = html;

let mut req = client
.post(&config.auth_url)
.post(https_url(&config.auth_url, "authorization")?)

let mut req = client
.post(&config.token_url)
.post(https_url(&config.token_url, "token")?)
Comment thread crates/oauth/src/flow.rs
let result = self
.client
.post(&self.config.token_url)
.post(https_url(&self.config.token_url, "token")?)
Comment thread crates/oauth/src/flow.rs
let result = self
.client
.post(&self.config.token_url)
.post(https_url(&self.config.token_url, "token")?)
let base = base_openai_tool_call_id(raw);
let mut candidate = base.clone();
let mut nonce = 1usize;
let mut suffix_index = 1usize;
Comment thread crates/vault/src/kdf.rs
}

fn test_salt(suffix: u8) -> Vec<u8> {
let mut salt = vec![b't'; 16];
Comment thread crates/vault/src/kdf.rs
p_cost: 1,
};
let salt = b"test-salt-16byte";
let salt = test_salt(b'a');
Comment thread crates/vault/src/kdf.rs
p_cost: 1,
};
let salt = b"test-salt-16byte";
let salt = test_salt(b'a');
Comment thread crates/vault/src/kdf.rs
let key1 = derive_key(b"password", b"salt-aaaaaaaaaaaa", &params).unwrap();
let key2 = derive_key(b"password", b"salt-bbbbbbbbbbbb", &params).unwrap();
let password = test_password("same");
let key1 = derive_key(&password, &test_salt(b'a'), &params).unwrap();
Comment thread crates/vault/src/kdf.rs
let key2 = derive_key(b"password", b"salt-bbbbbbbbbbbb", &params).unwrap();
let password = test_password("same");
let key1 = derive_key(&password, &test_salt(b'a'), &params).unwrap();
let key2 = derive_key(&password, &test_salt(b'b'), &params).unwrap();
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.

2 participants