Skip to content

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

#VectorStatus
1Cross-tenant access / IDORMitigated β€” app-scope + regression tests
2SSRF via customer URLsMitigated, one narrow bypass tracked (#3439)
3Malicious file uploadMitigated β€” scan gate, fail-closed
4Output-borne XSS (delivered artifact)Mitigated β€” sanitized at export
5Output-borne XSS (raw HTML endpoints)Gap β€” #3438
6AI prompt injectionPartly mitigated β€” no adversarial eval (#3440)
7Authentication / sessionMitigated
8AI data exposure (fail-open)In progress β€” #3422, #3423
9Transport / CORS / headersMitigated
10Dependency vulnerabilitiesMitigated β€” 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. getFileMeta filters .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; only http/https schemes are allowed.
  • Redirects are followed manually and each hop is re-validated (mcp/tools/url-crawler.ts fetchText), 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 strips script, iframe, object, embed, link, base, meta, all on* 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.ts stores the model’s HTML without calling sanitizeHtmlOutput.
  • PUT /api/files/:fileId/html stores user-supplied HTML verbatim (const html = await c.req.text(); ... objects.put(...)).
  • GET /api/files/:fileId/html/versions/:versionId serves it back as text/html; charset=utf-8 with no CSP, no X-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 user message (unpdf-claude-converter.ts), rather than instructions in system and 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.ts verifySupabaseJwt).
  • Protected routes additionally check profiles.deleted_at and 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-Policy is 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.yml present; 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.