Security Review
Round 2 · backendApi, capExpertApp and dbMigrations · 19 Oct 2026, updated 22 Oct
Verdict: critical. The round-1 fixes that landed hold up. Internal task endpoints now check a signed token in prod, but only log a failed check until enforcement is switched on, so they are not closed yet. Round 2 also found several ways for admin-side and vendor-side users to act beyond their role.
This public page leaves out file locations, endpoints, parameters and reproduction steps on purpose. The full report is kept off the web.
2 critical open2 from round 1 (1 partly fixed in prod)
+3 fixed in prod
+3 fixed in prod
15 high open5 new · 10 from round 1 (2 partly fixed)
+1 fixed in prod (R2-SQL-3, new) · +1 fixed in prod (B-H9)
+1 fixed in prod (R2-SQL-3, new) · +1 fixed in prod (B-H9)
53 medium open25 new · 28 from round 1 (4 partly fixed)
+2 not re-checked · 1 fixed in prod
+2 not re-checked · 1 fixed in prod
14+ low open14 new · most round-1 lows still open
On this page
What happens next
- This week: lock the internal job endpoints. Stop keeping credentials in the email log. Scope the admin-side password and role tools. Pin vendor identity on offers. Add the approver check to budget approval. Remove the leftover public file serving and the production API docs. Fix the front-end security headers.
- This sprint: make exported files and signed agreements private. Close the remaining cross-organisation reads. Add attempt limits and session revocation. Add row locks on checkout and offer acceptance. Put expiry on emailed links.
- This quarter: one shared tenant-scope guard, strict request validation everywhere, and safe SQL helpers. Rate limiting, least-privilege database and cloud identities, and encryption of stored credentials. Hardened container images and an audit log.
- Process: static analysis and secret scanning in CI, dependency gating, per-role authorization tests, and a security checklist in pull requests.
- Rotate the token-signing secret and the keys found in git history after the token fixes ship.
Critical and high
IDs starting with R2- are new this round; the others are from round 1.
| ID | Severity | Area | Finding | Status |
|---|---|---|---|---|
| B-C3 | Critical | Background tasks | Internal task endpoints accept unauthenticated requests | Partly fixed in prod |
| Why partial: every internal task is now signed and every task endpoint checks the signature. A failed check is still only logged, not refused, so older queued tasks keep running and the mobile API can sign its one remaining call first. It closes when enforcement is switched on. | ||||
| B-C7 | Critical | File storage | Signed links for other organisations' documents | Open |
| B-H11 | High | Admin user management | Partial admins can reset passwords | Open, worse |
| B-H14 | High | CoPilot | Bids and prices readable across organisations | Open, worse |
| R2-AUTHZ-1 | High | Admin user management | Some admin roles can change passwords and roles of users above them | Open |
| R2-DATA-1 | High | Email log | Credential emails are stored and readable by admins | Open |
| R2-STOR-1 | High | File storage | Exported files and signed agreements are stored publicly readable | Open |
| R2-BIZ-1 | High | Marketplace offers | A vendor can make offers in another vendor's name | Open |
| R2-BIZ-2 | High | CaptAIn / budgets | Assistant actions can approve budgets without approver rights | Open |
| B-H2 | High | Equipment planning | SQL injection in an admin bulk update | Open |
| Reproduced: confirmed on a local copy of the stack — an admin request reaches raw SQL through an unvalidated group value, with no data changed during the test. A separate error-handling bug in the same code hides the database error behind a generic failure, which makes this harder to spot in the logs. Both are open. | ||||
| B-H6 | High | Login | Password change without a session or attempt limit | Open |
| B-H8 | High | Capture | Room import into any project | Open |
| B-H10 | High | Chatbot | Chat file reference reaches any stored file | Open |
| B-H15 | High | Dashboard | Layouts changeable platform-wide | Open |
| F-H1 | High | Login | Organisation "require 2FA" not enforced | Open |
| B-H7 | High | Chat | Chat rooms readable without membership | Partly fixed |
| B-H12 | High | Onboarding | Registration links handed out too widely | Partly fixed |
| B-H9 | High | Document Manager | Any stored file path accepted | Fixed in prod |
Medium and low
IDs starting with R2- are new this round; the others are from round 1.
| ID | Severity | Area | Finding | Status |
|---|---|---|---|---|
| R2-SQL-2 | Medium | Equipment planning | SQL injection in an admin-only report filter | Open |
| R2-AUTHZ-2 | Medium | Marketplace | Vendors can read other organisations' equipment components | Open |
| R2-AUTHZ-3 | Medium | Transactions | Platform margin can be changed without admin rights | Open |
| R2-BIZ-3 | Medium | Checkout and offers | Double submission can create duplicate orders and offers | Open |
| R2-BIZ-4 | Medium | Marketplace | Sellers can set their own listing valuation | Open |
| R2-BIZ-5 | Medium | Shipping quotes | Shipping price can change after the buyer approved it | Open |
| R2-BIZ-6 | Medium | Document extraction | AI extraction can be pointed at other organisations' files | Open |
| R2-AI-1 | Medium | CoPilot AI summary | Bid text can steer the AI summary sent to hospitals | Open |
| R2-DOS-1 | Medium | API-wide | List sizes, query time and request rates are not limited | Open |
| R2-DOS-2 | Medium | AI features | No per-user limit on AI usage | Open |
| R2-DB-1 | Medium | Database | The application uses the database owner account | Open |
| R2-DB-2 | Medium | Database / CaptAIn | The analytics role's tenant boundary is incomplete | Open |
| R2-DB-3 | Medium | Database | Login links and third-party credentials are stored unencrypted | Open |
| R2-INJ-1 | Medium | User text is inserted into emails as HTML | Open | |
| R2-PHI-1 | Medium | Equipment photos | Photos flagged as containing patient information are not access-controlled | Open |
| R2-LOG-1 | Medium | Audit | Admin actions and data reads are not audit-logged | Open |
| R2-AUTH-1 | Medium | Login | Email lookups allow pattern searching of accounts | Open |
| R2-AUTH-2 | Medium | CoPilot chat email | Reply links in chat emails never expire | Open |
| R2-FE-1 | Medium | Front end | Rich-text cleaning still allows fake forms and page overlays | Open |
| R2-FE-2 | Medium | Front end | Security headers are missing on the main page | Open |
| R2-INFRA-1 | Medium | Cloud | A long-lived cloud key file is used at runtime | Open |
| R2-INFRA-2 | Medium | Containers | Images are outdated and run as root | Open |
| R2-INFRA-3 | Medium | API docs | The API documentation password can be bypassed | Open |
| R2-INFRA-4 | Medium | Dependencies | Backend package lockfile lacks integrity hashes | Open |
| B-M1–B-M25 | Medium | Backend, various | Of 25: 0 fixed, 4 partly fixed, 19 open (4 confirmed on the local stack, 3 worse than reported), 2 not re-checked | Mostly open |
| F-M2–F-M6 | Medium | Front end | Content policy, token handling, captcha, SSO, external links | Open |
| R2-L1–L14 | Low | Various | 14 hardening items: query escaping, retention, stored PINs and 2FA secrets, offer workflow edges, guard composition, third-party document viewers | Open |
| Lows | Low | Both repos | Nearly all still open; one closed by stricter input handling | Mostly open |
Fixed in prod
Deployed to production 22 Oct 2026.
| ID | Severity | Area | Finding | Status |
|---|---|---|---|---|
| B-C4 | Critical | Background jobs | Job runner accepts any Google service account | Fixed in prod |
| R2-SQL-1 | Critical | Background jobs | Internal job payloads reach database queries without validation | Fixed in prod |
| R2-SQL-3 | High | White Rabbit | Admin stickerId concatenated into SQL unvalidated | Fixed in prod |
| B-H9 | High | Document Manager | Any stored file path accepted | Fixed in prod |
| F-C1, F-H2 | Critical | Front end | Stored script injection in rich text | Fixed in prod |
| F-M1 | Medium | Login | Open redirect after login | Fixed in prod |
Scope and method
- Read-only review of backendApi (both roles), capExpertApp and dbMigrations, by seven parallel reviewers. Each critical and the main highs were re-checked by hand.
- Some findings were confirmed on a local copy of the stack, without changing data or sending messages. Nothing was tested against production.
- Every database query site in the backend was classified this round, about 2,700 in all.
- Not covered: mobile apps, extraction services, and cloud settings that can't be seen from code (IAM, ingress, bucket permissions, key restrictions).
- Status key: Fixed in prod deployed to production · Partly fixed · Open