This is a reference implementation and portfolio project, not an official service of any transit agency. It is deployed as a public demo, so it is built to be safe to expose, but treat it accordingly: no accounts, no authentication, and no database or application-log retention of rider text. The browser holds visible conversation turns for the current tab, and successful response payloads can remain briefly in a bounded serverless-container memory cache.
Please report suspected vulnerabilities privately rather than opening a public issue. Use GitHub's private vulnerability reporting for this repository, or email ckellyreif@gmail.com. A short proof of concept and the affected file help. There is no bounty; this is a personal project, and fixes are best effort.
The design treats a rider's question as untrusted input and the published corpus as the only source of truth.
- Data minimization. Input is checked for PII (ID numbers, contact details, birth dates) and refused before retrieval. Plaintext questions and history are not written to application logs, databases, or the answer cache; request logs carry only response kind, language, character count, timing, and operational flags (ADR 0004). The browser retains current-tab conversation turns, and the server cache retains successful answer payloads under process-local HMAC keys until eviction or container termination.
- No eligibility decisions. An output guard blocks and replaces any answer that decides a person's eligibility, and every answer must carry a citation that resolves to the corpus or it is not returned.
- Prompt-injection and scope guards. Inputs matching injection patterns or adjacent topics the assistant must not advise on (medical, immigration, legal) are refused and redirected.
- Browser output. Answer text is HTML-escaped before it reaches the DOM, citation fields are set as text, and citation links are validated to http(s) at the point an answer leaves the server, so a malformed corpus URL cannot inject a script-on-click link.
- Transport and headers. Responses set a
default-src 'none'CSP,nosniff,no-referrer, andno-store; the main page isx-frame-options: DENY. The HTML pages carry their CSS/JS in inline<style>/<script>blocks (no build step, no CDN), sostyle-src/script-srcallow only those blocks by their'sha256-…'hash — not'unsafe-inline'— and the hashes are recomputed from the served markup, so an injected inline script or a drifted policy is refused. There is no residual'unsafe-inline'; the pages carry no inlineon*=event handlers orstyle=""attributes (which hashes cannot cover). The embed adds a configurableframe-ancestorsin place ofx-frame-options(seeweb/handler.pyandweb/csp.py). - Cost and abuse bounds. A per-container request budget, Lambda reserved
concurrency, an API Gateway throttle, a 500-character question cap, a request
body size cap, and an IAM policy scoped to
bedrock:InvokeModelon the pinned model only.
The demo defaults are safe, but a real deployment should confirm each of these.
- Embedding. The embeddable widget (
/embed) defaults toframe-ancestors 'self'. To let agencies embed it, setFPA_EMBED_ANCESTORSto their origins, not a wildcard, and redeploy. - CI OIDC trust policy. The CI workflow assumes an AWS role by OIDC for live
eval runs. The workflow already refuses to assume it on fork pull requests, but
the role's trust policy is the real boundary: restrict its
subclaim to this repository'smainref (for examplerepo:OWNER/REPO:ref:refs/heads/main), not:pull_requestor a wildcard, so a fork cannot assume it. - Corpus pinning. Approve a corpus version in
corpus/CHANGELOG.mdand setFPA_PINNED_CORPUS_VERSION; the/versionendpoint then reports whether the running deploy matches. - Forged conversation history. A follow-up carries prior turns the client
holds; by default the server accepts any well-formed turn as context (this is
not a trust boundary — the output guard polices every new answer — but a
tampered prior "answer" can still be fed back as a leading premise). To
restrict history to turns this server actually issued, set
FPA_HISTORY_HMAC_KEY:/api/askthen returns an HMACsigover each successful answer plus the corpus/containment state, the client echoes it back with the turn, and the handler drops any turn whose signature does not verify. The production deploy script enables this by default and preserves the key across routine deploys; theconversationeval suite'sconv-forged-*cases assert the assistant re-grounds even when history is fabricated.
- Client-supplied conversation history. A follow-up request may carry prior
turns supplied by the client. This is context, not a trust boundary: the output
guard polices every new answer regardless of history, and there is no user
state to escalate against. A deployment that wants to remove the vector can
echo only prior questions, not client-provided answer text, or set
FPA_HISTORY_HMAC_KEYto restrict history to server-signed turns (see the deployment hardening checklist above). - The corpus is trusted input. Manifest URLs and snapshots are operator controlled and committed. Document ingestion (HTML and PDF) is not hardened against a hostile corpus; do not point the manifest at untrusted sources.