Skip to content

Security posture

This page is the whole security model in one place, including the parts a marketing page would soften. Read it before exposing anything to the internet.

Version 1 ships no auth of any kind. The system is split into a zone you must protect and a zone built to be exposed:

You protect: the editor and the management API

Section titled “You protect: the editor and the management API”

Anyone who can reach the editor UI or /api/v1/** can read every response, edit every questionnaire, download every file, and delete anything. The management API even sends permissive CORS headers — it fully assumes the network layer is the boundary.

Protect it with a reverse proxy that authenticates, a VPN, or network policy — concrete configs in Reverse proxy & exposure. This is the single most important operational fact about Forms-Engine.

Designed for exposure: the public runtime API

Section titled “Designed for exposure: the public runtime API”

/public/v1/** is what embedded questionnaires call, and its defenses are:

  • Unguessable, non-enumerable ids. A responseId is 128 bits of randomness; nothing lists responses publicly, and the reference-status endpoint never returns one.
  • Per-questionnaire allowed origins, enforced server-side on mutating requests (403) and via withheld CORS headers on reads. Origin checks keep other websites from embedding your questionnaire; they are not authentication, and requests without an Origin header (curl) pass.
  • Input caps everywhere: 500 answer keys / 256 KB per response, 10 KB per answer, 1 MB definitions, 128-char references.
  • Per-IP rate limits on mutations, uploads, geocoding, and reference status.

Holding a responseId is what it means to be a respondent session. Anyone with the id can read that response’s answers, modify them, and upload files to it — until completion freezes it. This is the accepted posture, consistent with the no-auth model; the protection is that ids are unguessable and never exposed anywhere except to the browser that created the session (where it sits in sessionStorage, not cookies).

Consequences worth stating outright:

  • Don’t log or forward response ids in your own systems more widely than you’d forward a session token.
  • Completion is the freeze point: after it, all respondent-side mutation returns 409.

Anyone holding a responseId can upload to that response, within the category allowlist, server-side content verification (magic bytes — the filename and claimed type are never trusted), per-file/per-response size caps, and the upload rate limit. Executables, scripts, HTML, and SVG are rejected in every category. Downloads exist only on the (network-protected) management API, always as attachments with nosniff — uploaded content is never served inline and never from a public endpoint, so a malicious upload can’t become a stored-XSS page on your domain. A no-op scanner hook exists for wiring in ClamAV or similar.

The external-ref mechanism deduplicates cooperative users and correlates responses with your records. It is spoofable by construction — the value comes from your page’s markup — and the reference-status endpoint necessarily reveals a completed-yes/no boolean per reference. The full analysis, including why cross-device resume deliberately doesn’t exist yet, is on External references.

  • Display text is sanitized by construction — author input is entity-escaped before a fixed safe tag set is generated; no raw HTML survives. Details.
  • CSV export guards against formula injection: text cells starting with =, +, -, or @ are quote-prefixed so respondent answers can’t execute in Excel.
  • Storage paths are server-generated ids only; no user string ever reaches a filesystem or object key.

These are roadmap items, not surprises — details on Roadmap & limitations:

  • No CAPTCHA / proof-of-work — rate limiting and origin checks are the current bot story.
  • No server-side rule re-validation — the public API validates answer shape and size, not questionnaire logic; a hand-crafted client can submit answers a hidden question would have cleared.
  • No signed references — hence the honor-system caveats above.
  • In-progress responses accumulate until you clean them up.

Report suspected vulnerabilities via GitHub issues on the tool repository — or, if the report is sensitive, use GitHub’s private “Report a vulnerability” flow on the repository’s Security tab if available rather than a public issue.