Attack Surface Review
Common attack vectors against the Gevell platform, the specific mitigation in
place for each, and β where a mitigation is missing or incomplete β a link to
the tracking issue. Evidence is cited to origin/main so this can be re-verified
rather than taken on trust.
Reviewed 2026-09-21. Re-run when the AI pipeline, the auth path, or the URL-fetch surface changes materially.
Summary
| # | Vector | Status |
|---|---|---|
| 1 | Cross-tenant access / IDOR | Mitigated β app-scope + regression tests |
| 2 | SSRF via customer URLs | Mitigated, one narrow bypass tracked (#3439) |
| 3 | Malicious file upload | Mitigated β scan gate, fail-closed |
| 4 | Output-borne XSS (delivered artifact) | Mitigated β sanitized at export |
| 5 | Output-borne XSS (raw HTML endpoints) | Gap β #3438 |
| 6 | AI prompt injection | Partly mitigated β no adversarial eval (#3440) |
| 7 | Authentication / session | Mitigated |
| 8 | AI data exposure (fail-open) | In progress β #3422, #3423 |
| 9 | Transport / CORS / headers | Mitigated |
| 10 | Dependency vulnerabilities | Mitigated β Dependabot + CI audit |
1. Cross-tenant access / IDOR
Risk. One account reads or mutates anotherβs documents, jobs, reports, or personnel data by guessing or altering identifiers.
Mitigations.
- Every record read is scoped by owner in the query itself, not just checked
afterward β e.g.
getFileMetafilters.eq('id', fileId).eq('user_id', userId)(workers/api/src/utils/file-list.ts). - Not-authorized reads return 404, not 403, so the boundary does not leak whether a resource exists.
- Regression tests lock both behaviors and are proven to fail if the owner
scope is removed (
__tests__/routes/files-tenant-isolation.test.ts).
Known limit. The tests exercise the application layer. The database row-level-security backstop the packet also claims is not yet covered by a test against a real Postgres instance β see #3441.
2. Server-Side Request Forgery (SSRF)
Risk. Audit and the ACR parser fetch customer-supplied URLs server-side; an
attacker points one at cloud metadata (169.254.169.254) or an internal host.
Mitigations.
isBlockedHost(mcp/tools/url-remediate.ts) rejects loopback, all RFC1918 ranges, link-local, the cloud-metadata address, ULA and link-local IPv6, and multicast; onlyhttp/httpsschemes are allowed.- Redirects are followed manually and each hop is re-validated
(
mcp/tools/url-crawler.tsfetchText), closing the βpublic URL 302s to a private oneβ bypass. The chain is capped at one redirect.
Gap. isBlockedHost only inspects dotted-quad IPv4 literals. Decimal, octal
or hex-encoded addresses (http://2130706433/ = 127.0.0.1), and hostnames that
resolve via DNS to a private IP (DNS rebinding), are not caught. β #3439.
3. Malicious file upload
Risk. A customer uploads malware, which lands in storage and is served to others or executed downstream.
Mitigations.
- Uploads are presigned directly to a quarantine staging bucket, never to
production storage (
routes/files.ts,services/scan-gate.ts). - AWS GuardDuty scans on arrival; only a clean verdict promotes the file to R2.
- Fail closed: threat, unsupported, access-denied, error, unknown status, or a scan that never returns all reject the upload.
- Presigned upload URLs expire in 600 s, locked by a regression test
(
__tests__/routes/files-upload-url-expiry.test.ts).
4. Output-borne XSS β delivered artifact
Risk. A crafted document (or the AI) produces HTML carrying <script>,
event handlers, or javascript: URLs; rendered in a victimβs browser it is
stored XSS.
Mitigations.
sanitizeHtmlOutput(utils/sanitize-output.ts) is an allow-list sanitizer that stripsscript,iframe,object,embed,link,base,meta, allon*handlers, and dangerous URI schemes. It runs with no DOM dependency so it works in Workers and Node.- It is wired into the export path and the remediate, reports, and
audit-report serve paths (
routes/export.ts,remediate-v2.ts,reports.ts,audit-report.ts,utils/ir-storage.ts). - The delivered accessible artifact is therefore sanitized before download.
5. Output-borne XSS β raw HTML endpoints (GAP)
Risk. The same HTML, before export, is stored and served raw.
What we found.
convert.tsstores the modelβs HTML without callingsanitizeHtmlOutput.PUT /api/files/:fileId/htmlstores user-supplied HTML verbatim (const html = await c.req.text(); ... objects.put(...)).GET /api/files/:fileId/html/versions/:versionIdserves it back astext/html; charset=utf-8with no CSP, noX-Content-Type-Options, no sandbox.
What limits it. The main preview renders in an iframe with
sandbox="allow-same-origin" and no allow-scripts
(apps/web/.../preview/[fileId]/PreviewClient.tsx), so scripts do not execute
there. Severity is therefore medium, not critical β but a user navigating
directly to the raw versions endpoint gets a top-level document on the API
origin with scripts enabled, which bypasses the frontend sandbox. β #3438.
6. AI prompt injection
Risk. Attacker-controlled text inside an uploaded document manipulates the model.
Mitigations / posture.
- The converters expose the model no tools and no actions β it transforms text to HTML with no memory across documents, so an injection can corrupt at most that one documentβs conversion; it cannot reach another tenant or trigger a side effect.
- Report Card is the exception, where model output feeds a grading decision; the mandatory human-review step is the backstop.
- Output sanitization (Β§4) neutralizes injection that tries to emit script.
Gaps.
- Instructions and untrusted document text are concatenated into one
usermessage (unpdf-claude-converter.ts), rather than instructions insystemand the document delimited as data. - There is no adversarial regression/eval corpus proving injection resistance. β #3440.
7. Authentication and session
Risk. Forged or replayed tokens; access after account deletion.
Mitigations.
- Supabase JWTs are verified with HMAC via WebCrypto against the configured
secret (
middleware/auth.tsverifySupabaseJwt). - Protected routes additionally check
profiles.deleted_atand reject soft-deleted users, with a short cache. - API keys are stored only as SHA-256 hashes (per the platform spec).
8. AI data exposure β fail-open (IN PROGRESS)
Risk. Where the AI-ingestion path was configured to redact and the redactor errors or its config canβt be read, text reaches the model unredacted.
Status. Two fixes in flight: #3422 (scrub errors must fail closed) and #3423 (config lookup must abort, not default to off). Note the productβs customer-facing masking claims have separately been removed pending a design decision; see #3429.
9. Transport, CORS, and headers
Mitigations.
- HTTPS/TLS 1.2+ only; no plaintext endpoint.
- CORS uses an origin allow-list function, not a wildcard (
index.ts,index-aws.ts). - A
Content-Security-Policyis set on the export and report serve paths (CSP_HEADER). Note this is not yet on the raw file-HTML endpoints β part of #3438.
10. Dependency vulnerabilities
Mitigations.
.github/dependabot.ymlpresent; weekly PRs.- CI runs a dependency vulnerability audit as a merge gate.
Tracking
Parent: #3442. Sub-issues #3438β#3441 are filed for the gaps above. The PII fail-open work (Β§8) is tracked separately under #3422 / #3423 / #3429 and is not duplicated here.