fix(mcp): real cited bibkeys in wiki_read + constant-time token compare

- C-3 (MED): wiki_read returned 'sources' = [concept_slug] (a placeholder), not
  the cited sources. Now parses the page's inline [BibKey #loc] citations and
  returns the actual set of cited bibkeys.
- S-2 (LOW): _TokenAuth compared the Bearer header with != (length/equality
  timing side-channel). Use secrets.compare_digest for constant-time comparison.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-06-15 16:34:28 +02:00
parent c4106b0f51
commit 77f9d79da0

View File

@@ -24,6 +24,7 @@ Transport
from __future__ import annotations from __future__ import annotations
import logging import logging
import secrets
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from mcp.server.transport_security import TransportSecuritySettings from mcp.server.transport_security import TransportSecuritySettings
@@ -121,7 +122,6 @@ def wiki_read(concept_slug: str) -> dict[str, object]:
directory or the requested page does not exist. directory or the requested page does not exist.
""" """
try: try:
import json
from pathlib import Path from pathlib import Path
from codex.config import get_settings from codex.config import get_settings
@@ -135,16 +135,12 @@ def wiki_read(concept_slug: str) -> dict[str, object]:
markdown = page_path.read_text(encoding="utf-8") markdown = page_path.read_text(encoding="utf-8")
# Collect cited bibkeys from the compile state # Collect the actual cited bibkeys from the page's inline [BibKey #loc]
state_path = wiki_dir / ".compile-state.json" # citations (audit C-3: this previously returned the concept slug itself,
sources: list[str] = [] # not the cited sources).
if state_path.exists(): from codex.wiki import _parse_claims
try:
state: dict[str, str] = json.loads(state_path.read_text(encoding="utf-8")) sources = sorted({claim.bibkey for claim in _parse_claims(markdown)})
if concept_slug in state:
sources = [concept_slug]
except (json.JSONDecodeError, OSError):
pass
return {"markdown": markdown, "sources": sources} return {"markdown": markdown, "sources": sources}
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
@@ -337,7 +333,9 @@ def main() -> None:
self, request: Request, call_next: RequestResponseEndpoint self, request: Request, call_next: RequestResponseEndpoint
) -> Response: ) -> Response:
auth = request.headers.get("Authorization", "") auth = request.headers.get("Authorization", "")
if auth != f"Bearer {token}": # Constant-time comparison to avoid a token-length/equality timing
# side-channel (audit S-2).
if not secrets.compare_digest(auth, f"Bearer {token}"):
return Response("Unauthorized", status_code=401) return Response("Unauthorized", status_code=401)
return await call_next(request) return await call_next(request)