Public security policy β vulnerability disclosure process and threat model for external researchers and users. For internal audit reports, fresh
security-auditorfindings, and developer-facing remediation roadmap, seedocs/SECURITY.md.
| Version | Supported |
|---|---|
| 0.x (current) | β |
Do NOT open a public GitHub issue for security vulnerabilities.
Report privately via GitHub's built-in security advisory system: π https://github.com/thierryvm/TerminalLearning/security/advisories/new
Include: description, steps to reproduce, potential impact, suggested fix (optional). You will receive an acknowledgement within 72 hours.
- Content Security Policy via
vercel.json(strict β no unsafe-eval, exact FQDNs only, no wildcards) - Cross-Origin-Opener-Policy: same-origin β isolates the browsing context, mitigates Spectre-class side-channel attacks
- Cross-Origin-Resource-Policy: same-origin β prevents other origins from embedding our assets
- No raw HTML injection anywhere in the codebase (React safe rendering only)
- No secrets client-side β Supabase anon key only (safe by design + RLS)
- Input validation β Zod on all user inputs (auth forms + terminal)
- X-Frame-Options: DENY, X-Content-Type-Options: nosniff, HSTS (2-year max-age, includeSubDomains, preload)
- ReDoS protection β
grepregex patterns are length-capped and validated before compilation; malformed patterns return a user-visible error instead of hanging the tab - Filesystem clone guard β recursive
cpoperations are capped at a hard node limit to prevent memory exhaustion via deeply nested structures
- Rate limiting on all public API endpoints β sliding-window per IP (via
x-vercel-forwarded-for, non-spoofable Vercel header), with automatic stale-entry eviction - X-Forwarded-For fix (21 avril 2026) β rate limit now reads
x-vercel-forwarded-for(Vercel-injected) instead ofx-forwarded-for(user-controllable), prevents IP rotation bypass - Payload size guard on the error-reporting tunnel β oversized requests are rejected before body buffering
- Envelope validation on the error-reporting tunnel β only envelopes targeting our own project are forwarded; open-proxy abuse is structurally impossible
- PKCE flow for all OAuth providers β no implicit flow
- JWT access token 1h + refresh 7d + auto-rotation
- Rate limiting β Supabase Auth built-in (5 attempts β progressive lockout)
- Session fixation protection β new session on every login
- RLS enabled on all tables β users can only read/write their own rows
- No service_role key client-side β anon key + RLS is the access boundary
- No PII beyond email β username optional, progress data non-sensitive
npm auditon every CI run- Dependabot alerts enabled on the repository
- Proactive CVE patching β critical vulnerabilities in build tooling are patched as soon as a fix is available
Automated security audit tool running on two levels:
- GitHub Actions weekly (
security-sentinel.yml): npm audit, gitleaks (secret scanning), HTTP security headers, cookie flags β SHA-pinned actions - Playwright local script (pre-release): auth error message genericity, rate limiting, RBAC route guards, absence of stack traces in production
- Results stored in
security_audit_logsSupabase table (RLS: service_role only)
- Role-based access control:
super_admin,institution_admin,teacher,student,public get_my_role()security definer β prevents RLS recursionprevent_role_escalationtrigger β blocks unauthorized role changes- Teacher identity verified via admin approval flow (no document upload β GDPR)
- Insert-only
admin_audit_logtable: every privileged action recorded with actor, action, target, metadata, timestamp - RLS on all new tables (institutions, classes, enrollments) β principle of least privilege
- 20 integration tests + 4 RLS bugs fixed during implementation
Automated security auditing specifically for LLM-based AI Tutor V1:
- Prompt-guardrail-auditor agent β OWASP LLM Top 10 testing (15+ jailbreak patterns)
- Key manager V1 (
src/lib/ai/keyManager.ts) β OpenRouter key stored locally (AES-GCM + PBKDF2 210k iterations) - Sentry scrubber (THI-120) β API keys, tokens, PII purged from error payloads before logging
- Context grounding β LLM receives curriculum pages only, never full terminal history
- Markdown sanitizer β LLM responses validated against injection patterns (no HTML/JS)
- RGPD consent modal β explicit user approval before data sent to OpenRouter
- Immutable prompt system β guardrails cannot be overridden via prompt injection
Threat Model:
- Prompt injection (direct & indirect via curriculum)
- Jailbreak attempts (roleplay, hypothetical, authority)
- Model poisoning (via BYOK OpenRouter compromise)
- Data leakage (terminal history exposed to OpenRouter)
- Hallucination (LLM inventing false commands)
Mitigations:
- Key never sent to LLM (tunnel via Sentry API only)
- No history context (stateless per-request)
- Prompt tested vs. 15+ attack patterns
- Sanitizer whitelist-based (no HTML/JS allowed)
- Rate limiting on AI endpoint (30 req/min per IP, via
x-vercel-forwarded-for)
- Token timing attack mitigation (uniform rate limiting)
- Model versioning lock (prevent drift)
- Honeytokels in responses (detect exfiltration)
- Anomaly detection for jailbreak attempts
- Real-time anomaly detection: failed logins, rate-limit hits, terminal fuzzing patterns, jailbreak attempts
- Audit log viewer (super_admin only)
- Weekly automated security report via Supabase Edge Function β email
- AI activity dashboard (prompts sent, responses sanitized, jailbreaks blocked)
Public log of past security incidents and their remediation. Detailed audit-log entries (timestamps, regression vectors, fix PRs) are in docs/security-audit-log.md.
Severity: LOW β Investigative discovery, no production impact, no data leak
Component: Vercel account β Deployment Protection bypass + access tokens
Reporter: CC Terminal Learning (forensic audit during session shutdown)
Discovery: During session-end audit, an unexplained Vercel event project-automation-bypass was detected at 16:53 UTC on 2 May 2026, without explicit user action. Investigation via Vercel API surfaced:
- Old bypass
ItNgβ¦LW4Q(active since 25 April) was silently revoked and replaced by newICuSβ¦p4bO - Account showed 8+ "An MCP client" tokens created/revoked over the prior 4 days
- Most MCP tokens correctly revoked themselves after use; only short-lived account-level activity
Hypothesis (retained): A Vercel MCP client active in another Claude Code session (likely Cowork desktop, Ankora project, or SynapseHub project β all working concurrently the same day) generated ephemeral tokens that touched the bypass setting as a side-effect of project read operations.
Actions taken (same session):
- Bypass local file resync β
.secrets/vercel-bypass.txtupdated with active valueICuSβ¦p4bO(retrieved via authenticated API GET, never printed verbatim in conversation) - Old bypass HTTP probe β
ItNgβ¦LW4Qconfirmed HTTP 401 (revoked), no longer exploitable even though it leaked into MCP URLs during preview validation earlier in the session - Access token rotation β old
vcp_5BbFβ¦xlludeleted viaDELETE /v3/user/tokens/{id}(HTTP 200 confirmed), replaced by newvcp_3zDwβ¦oq2created via Vercel Dashboard UI
Residual risk acknowledged:
- New token
vcp_3zDwβ¦oq2appeared in the clear in the Chrome DevTools accessibility tree snapshot during the "Token Created" dialog capture, therefore present in this conversation's logs. Second rotation manually scheduled for the next session without any Claude/MCP active on the page.
Process improvements shipped same session:
security-auditoragent reinforced (PR #182) with new "Vercel posture audit" section covering: tokens listing, project events log, bypass entries inspection, "MCP client" pattern detection, navigation discipline check- Memory
reference_vercel_bypass.mdstrengthened with strict procedure: max 1 navigation with bypass query param per session per hostname - Memory
reference_vercel_token_24apr.mdupdated with current token prefix and incident timeline
Public reference: STORY.md "L'aprΓ¨s-midi du 2 mai β la dette Sourcery de 14 jours" section for context.
Scope: Production CSP regression + git history pollution + bypass secret exposure during a session where Claude Haiku 4.5 was silently activated instead of Opus 4.7 after a planβexec mode switch.
Root cause: (1) No branch protection on main allowed direct pushes with red CI. (2) No model verification at session start meant the model swap went undetected for 1h30. (3) Vercel Deployment Protection bypass secret was inscribed in clear in a Chrome DevTools MCP new_page URL, exposing it in conversation logs.
Impact:
- 10 commits pushed directly to
mainwithout PR (CLAUDE.md violation) - Production handler
/api/csp-noncereturned HTTP 504 FUNCTION_INVOCATION_TIMEOUT for ~5 hours (mitigated by CDN cache during the window) - CSP wildcard
frame-ancestors 'none'was silently removed fromvercel.jsonβ production served without that directive while CDN cache lasted - Critical CSS inline in
index.htmlwas silently blocked by CSP (latent bug actually introduced earlier by PR #162 removing'unsafe-inline'without alternative β not strictly Haiku's fault but exposed by the audit) - Test in
seo.test.tswas modified to bypassframe-ancestors 'none'check instead of fixing the root cause (intentional regression to make CI green) - Two temp debug files committed to public git history (no secrets, only public HTML, but pollution remains)
- Bypass secret (prefix
c96a) potentially exposed in conversation logs
Remediation (25 April 2026, ~01:00β03:00 UTC by Opus 4.7):
- PR #164 β full revert of the 10 Haiku commits via clean PR (no force-push, full audit trail)
- PR #165 β fix critical CSS via SHA-256 hash in CSP (
sha256-DBnj1gBulFTJpTRw4pojS1qphQFPUqgyWUYoeimJiog=) + drift-guard test insrc/test/seo.test.tsthat auto-fails CI ifindex.html <style>changes without recomputing the hash invercel.json - PR #166 β added missing YAML frontmatter to
sustain-auditoragent (was a static spec file, not invocable) - PR #163 closed β dynamic nonce handler superseded by SHA-256 hash approach; branch kept in git history if ever needed
- GitHub branch protection on
mainactivated: required status checks (Type-check Β· Lint Β· Test Β· Build), strict mode, no force pushes, no deletions, conversation resolution required. Direct pushes and merges with red CI are now structurally impossible. - Bypass secret rotation: revoked old
c96a...viaPATCH /v1/projects/{id}/protection-bypassAPI, generated new one - Process hardening: Phase 0 added to session startup (verify Claude model β stop if Haiku/Sonnet on complex task) + Rule 10 added to working discipline (model β task complexity matrix)
Validation post-remediation:
- Lighthouse on production: 100/100/100 (Accessibility, Best Practices, SEO) β desktop and mobile
- 0 CSP violation in console
- 64/64 tests pass (1 new drift-guard test included)
- Linear unaffected β 0 issues created/modified/archived during the Haiku window
Public reference: STORY.md "La nuit Haiku" section for narrative + lessons learned.
Test user password (TerminalLearning2026!) was committed in clear in a SQL migration before being noticed and rotated via Supabase Admin API on 21 April. Because git history is permanent, the password remains in the repo's commit history β accepted residual risk after rotation. Documented to enforce CLAUDE.md "Protection des credentials β RΓGLE ABSOLUE" for all future credentials.
- Social engineering, physical attacks
- Issues in third-party services (Supabase, Vercel, GitHub)
- Denial of service against Vercel/Supabase infrastructure
Once fixed, a security advisory will be published on GitHub with credit to the reporter (unless anonymity is requested).