Security statement
What protects the data in Constructious, in plain words, and how each claim is checked. The full threat model and every review decision are in docs/architecture/security-model.md; this page is the summary to give a client or an auditor. Date of this version: 2026-10-24 (Sprint 16).
Who can get in
| Control | How it works | Evidence |
|---|---|---|
| Passwords | 12+ characters, checked against known breaches (k-anonymity range lookup: only five characters of a hash leave the server), stored only as one-way bcrypt hashes (cost from BCRYPT_ROUNDS, 12) |
LoginTest, PasswordResetTest |
| Two-factor | Time-based codes are required for Owner, Accountant and Partner (configurable). A password alone opens nothing: every route except sign-out, "who am I" and the two-factor steps refuses it. Codes cannot be replayed; guessing is throttled; recovery codes are single-use and hashed | TwoFactorTest, TwoFactorBypassTest (walks every route with a password-only session) |
| Lockout and rate limits | 5 failures lock that account-and-address pair for 1, 2, 4… up to 60 minutes (a stranger cannot lock the real user out); sign-in, uploads, exports, previews and the general API have separate limits | LoginTest, SecurityHardeningTest |
| Sessions | cookie sessions (HttpOnly, Secure, SameSite=Lax), idle timeout (default 30 min), absolute limit (12 h), CSRF token on every write | SessionIdleTimeout tests, tests/e2e/security.spec.ts (real server refuses a write without the token) |
| Sensitive actions | voiding, role changes, posting opening balances, exporting or pseudonymising a person need the password again; the confirmation is single-use and bound to the session | ReauthTest, OpeningBalanceImportTest, PseudonymisationTest |
What each person can see
- Documents, stock, money and labour belong to a site; a person sees only the sites they are assigned to, and only the money their role may see. A record of another site is refused or answers "not found" (most answer "not found", so its existence is not revealed). Suppliers, subcontractors, clients and materials are company-wide records, readable by the roles allowed to read them.
- Checked by
ScopeBypassTest: the whole route collection is walked as a supervisor, storekeeper, accountant and partner of another site over a company with documents everywhere; no list or screen shows anything of the first site, nothing changes data except the documented self-service routes, and nothing answers with a server error. It sends placeholder bodies, so a refusal for a bad body is not a proof of the policy; each module's own tests cover the policies with valid bodies. Each module also has its own isolation test. - Owner-only areas (users, settings, audit log, data protection, imports) refuse everyone else.
What is recorded
- An append-only audit log: database triggers refuse update and delete, and by default the application database user has no such right either (the weekly archive job needs one of the two options in
deployment.mdsection 5; with the second option the guard is the trigger alone for that job). It records sign-ins, failures, role changes, voids, exports, reads of sensitive screens and denied attempts, with device and address. - After the retention period rows move to an archive that is just as unchangeable; a live row can leave only when a copy identical in every column is already archived.
- A nightly integrity check proves the ledger balances and every stock balance equals its movements; on a failure (or if the check cannot run) it is recorded and the Owners get a security alert in the app and by email (if the mail queue is running). It does not by itself notice that it stopped running: watch the scheduler (
deployment.mdsection 7).
Protection of the data itself
- Encrypted in transit (HTTPS with HSTS). Secrets stored by the application (two-factor secrets, recovery material, bank details) are encrypted with the installation key. Backups are encrypted by
scripts/backup.sh(it refuses to run unencrypted, and keeps nothing if encryption fails);--no-encryptexists for local rehearsals only. - Uploaded photos and PDFs are private: stored outside the web root, type-checked by content, stripped of location data, served only through the application to people who may see the document, with short-lived signed links.
- Exports are written as the person who asked and nothing more; cells that a spreadsheet could run as a formula are written as text; downloads need a signed link that expires in 15 minutes; large exports are queued and capped.
- The browser app keeps no copy of business data: the service worker caches nothing and only shows an offline notice.
Headers and browser protection
Content-Security-Policy (scripts from the application only, no inline script, no eval), X-Frame-Options: DENY, X-Content-Type-Options, Referrer-Policy, Permissions-Policy on every response, including refusals and errors (TwoFactorBypassTest scans each API route). HSTS over https.
Privacy features
Search for a person, export everything held about them (and nothing about anyone else), pseudonymise them (name and contact detail replaced by a code; amounts, documents and the audit log keep their meaning), retention settings with archiving rather than deletion. Template notice: docs/product/privacy-notice-template.md.
Supply chain and operations
composer auditandnpm audit --omit=devreport no known vulnerabilities at release (2026-10-24).- The application database user cannot change or delete the audit log; deploy-time and backup users are separate (
deployment.mdsection 5). - The demo-account panel on the sign-in page exists only when
DEMO_CREDENTIALS_ON_LOGIN=trueandAPP_ENVis one oflocal,demo,e2e,testing(an allowlist: production, staging and any other name never show it); tests cover production, staging,Production,prod,liveand the allowed case. - Mail sent by the system never contains a password; links are built from the configured address, not from the request.
Known limits (accepted, tracked)
See security-model.md sections 22-26. In short: names in past audit rows and notification texts are not rewritten by pseudonymisation; an emailed or downloaded file leaves the system's control; crew members who share a name are one subject in a personal-data request; rate limits are per server (single-server design); the weekly archive job needs a database right that is documented in the deployment guide.
Reporting a problem
Send details to [security contact of the company running the system]. Do not include real personal data in the report.