Findings Register
| Owner | Classification | Version | Effective | Next review | Status |
|---|---|---|---|---|---|
| Sekhar Prakash | Internal | 1.11 | 2026-09-24 | 2026-11-27 | Active |
The consolidated worklist behind the posture maturity ratings. 54 findings (iteration 1 + re-verifies), reconciled against live GitHub issue state on 2026-09-10; the 12 open P1s re-verified against code on 2026-09-15 (blitz origin/main @ 78f1bde2, blitz-ui @ 4f6e16e3); F-34/F-35 closed and F-43–F-47 added 2026-09-22 from the GitHub CodeQL assessment. Each row links to its issue, where the full evidence (file:line) and fix detail live. F-25–F-28 came from the internal SECURITY-AND-PERFORMANCE-GAPS.md audit; F-29–F-33 from the completeness review; F-34–F-35 from the secret-scanning run; F-37 from a branch-protection review; F-39–F-41 from dev security work; F-42 from verifying the F-03 fix; F-43–F-47 from verifying the csharp half of the 2026-09-21 CodeQL assessment; F-48–F-51 from the 2026-09-22 full-codebase review; F-52 from the same reconciliation (a pre-existing hardening issue that had never been numbered) (blitz#1548 · blitz#1549).
Why the detail is in the issues, not here
This page is the register — id, severity, status, domain, and where it's tracked. The exact code locations and exploit walkthroughs stay in the linked GitHub issues (one click away). Same access boundary (finaisse-org GitHub org membership), but a single source of truth: the issue is authoritative, this table is the index.
Freshness — this register lags main by design
The portal is updated after a fix merges to main and is verified, not when the PR opens. A row therefore reflects the last reconciliation date in the header, not this instant. The live issue queries at the foot of this page are always current; where the two disagree, GitHub wins and this page is due a refresh. Last reconciled against live issue state: 2026-09-10. Last P1 code re-verification: 2026-09-15.
How to read. The Domain column links to the domain detail; re-verification history is in the Audit & Assessment Log.
Severity:
| Level | Meaning | Target |
|---|---|---|
| P0 | Exploitable now | ≤7d |
| P1 | High | ≤30d |
| P2 | Important, scheduled | ≤90d |
| P3 | Nice to have | best effort |
Status: Open, Partial, Resolved, or Risk-accepted.
P0 — Critical (must fix before production)
| ID | Finding | Status | Domain | Tracked by |
|---|---|---|---|---|
| F-04 | Permissive CORS (origin:()=>true + credentials) on all services incl. admin apimgmt; CSRF-able | Open | 6 | blitz#999 |
| F-25 | Mass-assignment of identity/audit fields. Re-measured 2026-09-23 with a runtime scan that reads each route's resolved schema (the fields a caller can actually set) instead of matching source text. blitz#1563 fixed or disproved all 55 routes on the 2026-09-22 list, but the real scope was about 3× larger: 39 Tier-1 routes (certification/approval actors such as certifiedby and decertificationrequestedby, plus direct writes to audit and history tables) and 2 approval writes with no trusted actor (recon decertifyAccount takes the actor from the body; the JV bulkAction approve records none and skips SoD) keep this at P0. The 47 Tier-2 routes are split out as F-54. Guarded by lint:security R1 (runtime scan, baseline 41, ratchet) since blitz#1568; closes when R1 = 0 | Partial | 16 · 4 | blitz#1014 |
| F-26 | Arbitrary-field filtering → BOLA / cross-user data enumeration. Re-verified 2026-08-27 (HEAD d8c1eaec) — still open, but the surface moved. #1359 added ~40 scoped endpoints and #1370 commented out the generic POST /filter across all 325 controllers (0 live /filter routes; queryFilterSchema no longer bound to any live /filter). But the same vulnerable where path is still reachable through the scoped replacements — /lookupbyname (and ~20+ others) still take filter: t.Optional(queryFilterSchema), and the service spreads the client where straight into Prisma (whereClause = { ...scopeClause, ...(where?.where ?? {}) } in customer.service.ts:298). querybuilder.ts field: t.Optional(t.String()) still unallowlisted. The fix relocated the hole, didn't close it. Closing needs the where predicate path centrally allowlisted (or removed from the scoped endpoints) | Partial | 6 · 7 | blitz#1015 |
| F-41 | No brute-force protection on signin — /auth/login has no failed-attempt lockout or rate limiting (the elysia-rate-limit limiter is wired only into the agents app, not the api app that serves signin) → credential-stuffing / password-spray ✅ RESOLVED — verified 2026-09-22. LoginThrottleService (systema/services/system/loginthrottle.service.ts) is implemented and wired into /auth/login with pair/account/IP counters; the 429 reuses the GENERIC_LOGIN_FAILURE body so it does not leak account existence. blitz#1352 is CLOSED. The register had drifted — the issue closed but this row was never updated. | Resolved | 4 · 6 | blitz#1352 |
P1 — High
| ID | Finding | Status | Domain | Tracked by |
|---|---|---|---|---|
| F-05 | No HTTP security headers on any service or frontend nginx (no CSP/HSTS/X-Frame-Options). Re-verified 2026-09-15 — still nothing at any layer: no helmet-equivalent in any Elysia service, nginx sets only Cache-Control, and no response-header transform rule exists in fin-infra/cloudflare/. Scoping answered: static headers (HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy) can be owned at the edge (Cloudflare transform rule now; CloudFront SecurityHeadersConfig on AWS), but CSP cannot — it is per-app and needs nonces/hashes to meet the no-unsafe-inline bar, so it belongs at nginx. Edge-only is also bypassable via a raw *.up.railway.app domain (the F-38 class), so the origin must set them too | Open | 3 · 5 | blitz#1000 |
| F-07 | Auth tokens in JS-readable cookies (no httpOnly/Secure/SameSite); no refresh rotation. Re-verified 2026-09-15 — the 2026-09-09 note that tokens are 'no longer cookies' was only half the system and is corrected here. The API returns tokens in the JSON body, but blitz-ui still writes them to JS-readable cookies — useCookie('accessToken') across ~16 files; @core/composable/useCookie.ts sets path/maxAge only, no httpOnly/Secure/SameSite. Against the 2026-07-11 target (5-min access token): actual is 24h (systema/utils/constants.ts:1-2). POST /refreshToken mints a new pair but never invalidates the presented token and keeps no issued/revoked store, so every refresh token stays valid for its full 7 days in parallel, with no reuse detection | Open | 4 | blitz#1001 |
| F-08 | Stored XSS via unsanitized v-html on email bodies + AI content (no DOMPurify). Re-verified 2026-09-15 — no sanitizer of any kind in the repo (zero hits for DOMPurify/sanitize-html across *.ts/*.vue/package.json); v-html in 33 files. The four untrusted sinks are unchanged: selectedNotification.bodyHtml (collections inbox :782), selectedNotification.message + finniSummary (cashapp inbox :666/:415), previewEmailBody (NotificationTemplateDetail.vue:865). Email bodies are attacker-controlled by definition, so this is reachable without prior tenant access. Chains with F-07 (JS-readable token) and F-05 (no CSP): unauthenticated email → stored XSS → session theft → 7-day durable session, with no mitigating layer at any step | Open | 5 | blitz-ui#657 |
| F-09 | API docs (Swagger/Scalar) served without auth in non-dev environments | Open | 6 | blitz#1002 |
| F-10 | Temporal client TLS hardcoded false (lower risk on Railway's encrypted private net; must fix for AWS). Re-verified 2026-09-15 — wider than filed: three production sites, not one. libs/wfc/src/workflowclient.service.ts:64 (the filed site), libs/wfw/src/workflowworker.service.ts:99, apps/wfwpdf/src/workflowworker.service.ts:50 — fixing only the client leaves both workers (where activity payloads actually flow) on plaintext. No TEMPORAL_TLS var exists in fin-infra IaC; bapiproxy needs a TemporalOptions__* equivalent as it ignores shared vars. Sequencing constraint: the Temporal server must terminate TLS (clustered/manual setup + certs) before the code default flips, or every worker connection breaks | Open | 3 | blitz#1003 |
| F-11 | AWS Day-1 detection baseline missing (CloudTrail, GuardDuty, Security Hub, VPC Flow Logs, WAF) — go-live blocker | Open | 9 · 11 | fin-infra#31 |
| F-12 | AWS IAM resource scopes too broad (kms:Decrypt on *, ECS/CloudFront on *) | Open | 4 · 9 | fin-infra#32 |
| F-13 | Resolved | 3 | fin-infra#33 | |
| F-23 | No multi-factor authentication for application users (esp. admin/approver roles). Re-verified 2026-09-15 — not started. No better-auth dependency or import in either repo; zero hits for TOTP/WebAuthn/passkey/OTP; auth remains password-only for every role. The 2026-07-11 betterauth decision has had no code movement in two months. Shares a solution with F-07 (betterauth would deliver MFA and httpOnly sessions + rotating refresh) but remains a separate control (D4-02 vs D4-04) and a separate SSO/SAML vendor decision — tracked separately by design | Open | 4 | blitz#1007 |
| F-24 | No independent validation — establish penetration test + vulnerability disclosure (VDP) program | Open | 14 | fin-infra#39 |
| F-27 | Pagination: itemsPerPage capped at max 1000; take/skip still unbounded. Re-verified 2026-09-15 — unchanged. base.ts:26-28 is bounded; querybuilder.ts:24 take still has no maximum and :23 skip has neither maximum nor minimum (negatives accepted). queryFilterSchema is imported by 327 files, so every controller using it is still unbounded. Decision 2026-09-15: keep the 1000 cap and apply the same bound to queryFilterSchema. Caveat for that change: frontend stores requesting take: 10000 will start 400-ing, so blitz-ui call sites need fixing in the same pull | Partial | 6 | blitz#1016 |
| F-28 | JWT accepted via query param → token leakage to logs. Re-verified 2026-09-15 — still present at authenticator.ts:102 (inbearer ?? query.token, the only such fallback), and the root cause is broader than the transport. The app logs tokens itself, so removing the query fallback does not close the leak: (a) signin.controller.ts:252 logs the whole request body at INFO on /refreshToken, where the body is the refresh token — a 7-day, non-rotating credential (F-07) written to routine log volume in plaintext; (b) authenticator.ts:219 logs { bearer } unredacted, while its neighbours at :128/:228 use safeTrim (7-char truncation). No pino redact config exists anywhere in the repo. Both sites folded into blitz#1017 rather than filed separately (triage 2026-09-15) | Open | 4 · 11 | blitz#1017 |
| F-29 | 5 open dependency CVEs in blitz (0 critical, 4 high) — re-measured 2026-09-15 vs the committed lockfile (was 97 on 2026-07-18; 114 on an unpinned tree). The protobufjs RCE is gone. SCA now runs weekly, non-blocking (Control Register); gated at critical, which blitz currently passes | Open | 8 | blitz#1018 |
| F-30 | ⚠️ REGRESSED — 161 open dependency CVEs in blitz-ui (4 critical, 93 high, 58 moderate, 6 low) — re-measured 2026-09-15 vs the committed lockfile; was 93 (2 critical) on 2026-07-18. Only swiper (prototype pollution) ships to the browser; happy-dom/node-tar/shell-quote are dev/build-time and bun audit has no --production flag to separate them. SCA now runs weekly but is report-only here (Control Register) — a real gate would be permanently red until this backlog is triaged. Raise repo variable AUDIT_GATE_LEVEL once it is | Open | 8 | blitz-ui#667 |
| F-31 | No automated security scanning in CI (SCA/SAST/secret/IaC/image) — establish the program | Open | 14 · 8 | fin-infra#40 |
| F-34 | Secrets in blitz-ui git history — re-confirmed 2026-09-10 (gitleaks, 2036 commits): 8 hits = 1 gcp-api-key + 7 generic-api-key. The GCP key sits in src/apps/ui/.env.production at commit 102da79 (2025-12-30); it was removed from HEAD in 16af89d and the path is now gitignored, but the blob remains reachable in history — a .gitignore prevents the next commit, it does not scrub the last one. Same key as F-35 (verified by hash comparison) — one credential, two repositories. Remediation is one regeneration (self-serve; the Gemini licence is billing-account-level, not key-level) + history rewrite in both repos (filter-repo/BFG). Note this was a client-side build var, so key referrer/API restrictions are the durable control, not secrecy. ✅ GCP key REGENERATED — confirmed by Sekhar Prakash 2026-09-15, which kills the exposed credential and closes the urgent half of both F-34 and F-35. Status moves Open → Partial: the blob remains reachable in blitz-ui history (now a dead string), and .gitleaks.toml is still absent, so the next scan will re-report the same 8 hits. ✅ CLOSED 2026-09-22. The open history-rewrite decision is settled: rewrite DECLINED, reasons recorded on the issue — it would rewrite 2,628 commits in blitz-ui / 3,978 in blitz and invalidate all 73 release tags (every tag is downstream of the leak) while release.json pins images to tag and digest, for zero security gain on a dead credential; forks and cached views can retain blobs regardless. ✅ The client-side exposure is structurally gone: verified 2026-09-22 that GOOGLE_API_KEY no longer exists anywhere in blitz-ui — it is consumed server-side only in blitz (libs/agentsbase, libs/docai). Consequently referrer restrictions no longer apply; the relevant control is now an API restriction (limit the key to the Generative Language API) — still unverified, and the one open action. Re-triaged the 8 gitleaks hits 2026-09-22: 0 real — Syncfusion licence keys (client-bundle by design), the dead GCP key in untracked, gitignored .env files (git log shows 0 commits touching them), Vuetify template mock "Server Key 1/2/3" UUIDs dated 2020/2021, and a dataKey field name. ⚠️ Scope note: GitHub's server-side scan sees 2 secrets org-wide; a local working-tree scan sees ~38, because most hits are in files that were never pushed. The numbers are not comparable | Closed | 4 · 8 | blitz-ui#668 |
| F-35 | Secrets in blitz git history — re-confirmed 2026-09-10 (gitleaks, 2785 commits, 809 MB): 39 hits = 17 generic-api-key · 10 linkedin-client-id · 8 linkedin-client-secret · 3 jwt · 1 gcp-api-key. The GCP key is in railway-iac/shared/variables.ts at commit 71f9f15 (2025-12-31); railway-iac/ was later migrated out to fin-infra (29f106a) so the path is gone from HEAD, but the blob remains reachable in history. ⚠️ It is the SAME key as F-34 (verified by hash comparison, not value) — one credential exposed in two repositories, so a single regeneration closes both, and both histories need scrubbing. The LinkedIn client id/secret pairs are a second credential class not previously called out. ✅ GCP key REGENERATED — confirmed by Sekhar Prakash 2026-09-15 (one regeneration closes the GCP half of both F-34 and F-35). Status moves Open → Partial. Verified 2026-09-15 against origin/main @ 78f1bde2: .claude/settings.local.json is now untracked and gitignored at any depth ✅; .gitleaks.toml still absent ❌; the dead RabbitMQ files src/build/mq/{definitions.json,enabled_plugins,rabbitmq.conf} are still tracked ❌. 🔴 CORRECTION 2026-09-22 — there is NO LinkedIn credential. The 18 linkedin-client-id/linkedin-client-secret hits are false positives: they match the cash/remittance column names linkedinvoiceids, linkedinvoices, linkedinvoiceamount on the substring linked+invoice. Confirmed from gitleaks 8.30.1 output — match: "linkedinvoiceids: 'linkedinvoiceids'". There is no LinkedIn integration in this codebase (zero hits for linkedin.com, api.linkedin, or any OAuth client). No rotation is required and none was ever required; earlier register versions describing this as a second exposed credential class were wrong. The remaining 39 hits re-triaged 2026-09-22: 0 real — 3 JWTs decoded to sub all-zeros / empty tenantId, expired 2026-01-16 (test fixtures in zplayground/); Syncfusion licence keys; the dead GCP key in untracked, gitignored .env files (0 commits); and one RabbitMQ password_hash in tracked src/build/mq/definitions.json — a SHA-256 hash, not plaintext, and RabbitMQ is dead per CLAUDE.md (cleanup candidate, not an incident). ✅ CLOSED 2026-09-22 — history rewrite declined (see F-34 for the blast-radius reasoning) | Closed | 4 · 8 | blitz#1019 |
| F-37 | No branch protection on main in blitz and blitz-ui — force-push and unreviewed merge possible, no required status checks. 2026-09-24: the org-level ruleset DefaultBranchProtection now covers blitz, blitz-ui, fin-infra and fin-internal-docs, so force-push, branch deletion and direct push are blocked (fin-infra#55 closed 2026-09-09). Still open: unreviewed merge (0 required approvals, kept by owner decision 2026-09-24); no required status checks (removed 2026-09-15 with the CI guards); finwebsite and fin-works are outside the ruleset | Partial | 14 · 8 | fin-infra#55 |
| F-38 | Temporal workflow visibility not tenant-scoped — /wf routes return executions across all tenants (tenant id is a workflow arg, not a search attribute). Closed 2026-08-09 as verified not currently exploitable (staging inherits finance's empty permission catalogue via the tenant_base template clone, so no tenant-side role holds system:workflow:*). Not a code fix — the scoping gap remains and reactivates the moment RBAC is seeded; re-open then and fix before/with the RBAC seeding | Risk-accepted | 7 · 11 | blitz#1206 |
| F-42 | Raw-SQL passthrough in the recon filter primitive — recon.filter.ts passes a non-JSON filter string through as raw SQL by its own documented contract (values in structured JSON filters are correctly bound), and quoteField escapes quotes without a ^[A-Za-z0-9_]+$ allowlist like the one added for F-03. Found while verifying the F-03 fix on main (5aff94c5) — same pull. Reachability CONFIRMED 2026-09-15 — the 'needs runtime confirmation' caveat is resolved: the path IS tenant-reachable. Chain: source_filter/target_filter are declared t.Any() (no boundary validation) in createMatchingConfigSchema/editMatchingConfigSchema (matching_config.ts:46,78; same in matching_rule.ts:14,37) → exposed on ordinary tenant-authenticated write routes (matching_config.controller.ts .post:84, .put:106, not admin-only) → passed unmodified to resolveSqlFilter (recon.service.ts:275,294; recon.matcher.ts:1891,1899) → returned as a raw SQL predicate at recon.filter.ts:41-50. quoteField (:114) still lacks an allowlist. Values inside structured JSON filters are correctly bound — the exposure is the identifier path and the string-passthrough branch. Severity kept at P1 (triage 2026-09-15): tenant-reachable and structurally identical to F-03, but requires a user with recon-config write rights, so narrower than an unauthenticated surface ⚠️ F-number collision, resolved 2026-09-22 — this row is correct and stands. blitz#1388 (platform error-handling architecture) states in its body that it is "tracked as finding F-42", but that reference is stale and self-assigned: #1388 was filed 2026-08-30 and has never appeared in this register (verified across full file history). F-42 was allocated here on 2026-09-10 (fin-internal-docs#87) to blitz#1460, filed 2026-09-09. No renumbering is needed — the fix is a one-line correction to #1388's body, and nothing external references "#1388 = F-42". Under the stated rule #1388 would not take an F-number in any case: it is an architecture decision (422-instead-of-404 across 328 by-id endpoints, dev-mode detail leak) with a real security consequence but no specific exploitable weakness | Open | 5 · 6 | blitz#1460 |
| F-43 | ExcelRW controllers are unauthenticated — all four carry [AllowAnonymous] (ExcelServicesController.cs:13, FileConversionController.cs:11, TaskRoasterController.cs:12, VersionController.cs:31) and accept IFormFile uploads plus [FromBody] storage paths. This is the gate that makes F-44/F-45 reachable. ⚠️ Mitigating scope: blitz-excelrw has no public domain (verified against fin-infra railway/shared/domains.ts, which lists only blitz-ui, blitz-mgmtui, temporal-ui), so exploitation requires access inside the private network — this is why it is P1 and not P0. Two changes flip it to P0: a public domain attached to blitz-excelrw, or an SSRF in a public service able to reach .railway.internal. ⚠️ Do not simply add [Authorize] — wfw and workflow activities call ExcelRW internally, so this needs an internal-service-auth decision or a documented private-network acceptance | Open | 1 · 7 | blitz#1549 |
| F-44 | Cross-tenant object-key traversal in ExcelRW storage — ExcelRW/Services/MinIO.Storage.cs:24 (download) and :45 (upload) set Key = path.TrimStart('/'), which strips leading slashes but does nothing about ../. path is the second half of a client-supplied bucket:path string, and ParseFilePath returns the caller-supplied bucket too — so both the bucket and the key are request-chosen, giving cross-tenant object read and write. Staging uses per-tenant RustFS buckets, so the blast radius is other tenants' financial documents. Reached from 8 ParseFilePath call sites on the anonymous controllers. Highest-impact item in the ExcelRW set and the cheapest real fix (reject .., canonicalize, validate bucket against the caller's tenant; no API change) | Open | 7 · 4 | blitz#1549 |
| F-45 | Arbitrary assembly load from an uploaded Razor template — OpsCommon/Razor/RazorUtils.cs:286-287,291; LookupAssembly (:303-322) does File.Exists + Path.GetFullPath with zero path validation, accepting absolute paths and traversal, and the assembly name/path is read from ///r: directives in the template body. Chains with F-44: the template itself is fetched through the traversable object key (ExcelServiceService.Export.cs:92-96), so F-44 controls which template F-45 compiles. ⚠️ Honest framing: the feature is arbitrary Razor/C# compilation from an upload by design — fixing Assembly.LoadFrom alone does not close it; the question is whether anonymous arbitrary-compile is acceptable even on the private network. By severity this is the highest-severity open finding in the register; it is P1 on reachability (see F-43) | Open | 1 · 7 | blitz#1549 |
| F-46 | Unparameterized SQL in the DuckDB library builders — Common.DuckDb/ExcelOpsExtensionsCsv.cs:25-35,50-53 and ExcelOpsExtensionsExcel.cs:25,32-36,54-59,79-82 interpolate tableName (raw identifier), normalizedPath, sheetName and dateFormat into SQL with no parameters; the only transform is Replace('\\','/'), which never escapes ', and the value sits inside a '...' literal. ExcelOpsExtensionsExcel.cs:25 executes a caller-supplied SQL string verbatim. ⚠️ Do not dismiss as unreachable — these five methods have no in-repo callers and look like dead code, but they are public static extension methods on IDbConnection intended to be called from Razor templates (ExcelOps.Connection is public at ExcelProcessor/ExcelOps.Db.cs:11). Real injection flaws in a library API; the F-45 surface dominates | Open | 3 · 7 | blitz#1549 |
| F-47 | Per-tenant SAP connection profile logged at Information level — BapiProxy/Controllers/BaseBapiControllerBase.cs:225-226 logs the whole SapConnectionOptions, a C# record whose auto-generated ToString() enumerates every property including Password (BapiConnector/Options/SapConnectionOptions.cs:31, assigned from decrypted credentials at :220). Stated precisely: the password does NOT leak today — the property is a SecureString, whose ToString() returns the type name, not the plaintext. What does leak per tenant is IpAddress, Username, SystemID, SystemNumber, Client, LogonGroup. The fragility is that it is one type change away from leaking the password. Small contained fix: log named scalar fields or give the record a redacting ToString() | Open | 7 · 11 | blitz#1549 |
| F-49 | Conversation IDOR cluster — cross-user read, rewrite and ownership takeover. conversation.service.ts:206 updates with an unscoped where:{id} then stamps createdby: userId, so the victim's conversation is not merely edited but transferred; me.conversations.controller.ts:187 destructures :id then never uses it, looking up by itemid alone on an autoincrement() PK; POST :id/participants takes conversationid from the body, manufacturing the very membership row that conversationitem.service.ts:161 relies on as its one guard. ⚠️ The pattern was known and skipped — the read paths in the same file (:49-58, :221) check ownership correctly. Live on the public gateway; conversations carry contextid/contexttype linking to close/recon/invoice entities | Open | 7 · 4 | blitz#1560 |
| F-50 | Token design: one JWTSECRET across four token families, no revocation. authenticator.ts:63-86 derives access, refresh and setup tokens from the same secret (the five OAuth state tokens too), and every verify() in the repo is called with no options, so aud/iss/typ are never checked and the tokens do not carry them. Consequences traced: a 7-day setup-password invite token is a valid access token (:218 reads only sub, ignores purpose; the rbac macro resolves permissions from the DB, not the JWT), and a refresh token works directly as an access token. Compounding: logout is a no-op (signin.controller.ts:417 destructures bearer and discards it) — no denylist, no jti, no tokenversion, so disabling an account revokes nothing; and refresh tokens are never rotated, so one theft is a permanent foothold. ⚠️ signin.controller.ts:358 logs the full refresh token in cleartext on every refresh | Open | 7 · 4 | blitz#1560 |
| F-51 | AWS GitHub OIDC trust policy accepts any ref — aws/global/iam/github_oidc.tf:23-27 scopes token.actions.githubusercontent.com:sub to repo:<org>/<repo>:*, matching every branch, tag and PR in blitz, blitz-ui and fin-infra. The role grants secretsmanager:GetSecretValue on finaisse/* — holding finaisse/jwt-secret, finaisse/database-url and finaisse/management-database-url (aws/modules/secrets/main.tf:44-51) — plus iam:PassRole on finaisse-ecs-*. Role is applied. ✅ Mitigating: no workflow currently assumes it (grepped all three repos for configure-aws-credentials, zero hits), so this is latent. Compounds with F-37. One-line fix: pin to :ref:refs/heads/main / :environment:production | Open | 4 · 14 | blitz#1560 |
| F-53 | SSO first sign-in binds to a platform user by email alone, and the system identity is eligible. bindableProfile (systema/identity/auth.ts) matches sys.user by email, case-insensitively. The seeded root row (all-zeros id, root@finaisse.com) is created without flags, so it defaults to interactive and passes mayHoldSession. Nothing refuses SYSTEM_USER_ID, and there is no email_verified check or provider-domain enforcement at binding. An identity provider asserting root's address therefore gets full access. Needs control of an operator-configured tenant IdP (/sso/register is disabled), which is why it is P1 not P0. Found while verifying F-48 | Open | 4 | blitz#1573 |
P2 — Medium
| ID | Finding | Status | Domain | Tracked by |
|---|---|---|---|---|
| F-06 | Rate limiting only on agents — auth/login + main API unthrottled (Railway Fastly gives some edge protection) | Open | 6 | blitz#670 |
| F-14 | AI/agent surface: PII to LLM with no redaction, shared key (role-spoof part covered by P0 #671) | Open | 5 · 7 | blitz#669 |
| F-15 | Incomplete audit trail on financial records; no tamper-evidence / signing | Open | 16 | blitz#1008 |
| F-16 | Server-side state-transition guards + segregation-of-duties unverified | Open | 17 | blitz#1009 |
| F-17 | No SPF/DKIM/DMARC; unthrottled email-triggered workflows; attachment parsing risk | Open | 3 · 5 | blitz#1004 |
| F-18 | Encryption uses AWS-managed keys only; no CMKs, no S3 TLS-only policy, no secret rotation | Open | 2 · 9 | fin-infra#34 |
| F-19 | Unbounded tenant Prisma client / pg-pool caches (no TTL/eviction) | Open | 7 | blitz#1005 |
| F-20 | No container image scanning / signing / digest pinning; :latest tags | Open | 8 · 10 | fin-infra#35 |
| F-21 | No pre-commit secret scanning; dead GOOGLE_API_KEY in UI build | Open | 4 | blitz#1006 |
| F-22 | CI/CD + account hardening: no Terraform plan gate, no Identity Center/MFA, no break-glass, single NAT | Open | 14 · 9 | fin-infra#36 |
| F-32 | No endpoint/workstation security baseline (disk encryption, MDM, EDR) — developer laptops are the perimeter | Open | 19 | fin-infra#41 |
| F-33 | No security-awareness / people-security program (training, phishing sim, joiner/leaver) | Open | 20 | fin-infra#42 |
| F-01 | x-blitz-tenant-id header — not bound to JWT tenant claimtenantbinding.ts): tenantInfoResolver (onRequest, against the unverified claim, so a forged token never selects a Prisma client) and the authenticator (derive, against the verified payload compared to the resolved tenant, covering the Host-resolved path too). Fails closed on an unresolved tenant. RLS backstop remains outstanding — tracked under D7-02 (Resolved 2026-09-04) | Resolved | 7 | blitz#992 |
| F-03 | matching_output.service.ts (closed 2026-09-09). Follow-on gap raised as F-42 in the recon filter primitive added by the same fix | Resolved | 5 | blitz#998 |
| F-39 | teammember.filters.service.ts string-interpolated $queryRawUnsafe | Resolved | 5 | blitz#1350 |
| F-40 | Bun.password.hash (memory-hard, ~150 ms/hash). Stored PHC strings self-declare their scheme, so legacy SHA-256 rows are verified once and upgraded in place on next successful login (UserService.verifyCredentials). Legacy getHash retained only for that migration path and for apikey.service — see F-43 (closed 2026-09-09) | Resolved | 4 | blitz#1351 |
| F-36 | bun.lock gitignored → non-reproducible builds + unreliable dependency scanning--frozen-lockfile enforced across all Docker/CI builds in all 3 repos, reusable lockfile guard blocks regressions (closed 2026-07-18) | Resolved | 8 | blitz#1072 · blitz-ui#694 |
| F-02 | requirePermission now enforces server-side (403 by default; PERMISSIONS_ENFORCE !== "false"), permission catalogue + role grants seeded (finbased/prisma/seed.ts); frontend route guards in place (closed 2026-08-06). ⚠️ 2026-09-22 — the commit that closed this introduced F-48. c53f0c1f2 (blitz#1033, 2026-07-11) added the requirePermission enforcement recorded here and the username === "root" → GRANT_ALL break-glass that F-48 exploits. Enforcement genuinely works; the bypass defeats it. Stated because it generalises: a Resolved entry is evidence about the OLD bug, not a clean bill of health for the code that fixed it — remediation commits warrant the same adversarial review as features, arguably more, since they touch security-critical paths by definition | Resolved | 4 | blitz#671 · blitz-ui#314 |
| F-48 | /auth/signup before the authenticator + free-form userName + a lowercasing username === "root" bypassrequirepermission.service.ts, requireapprole.service.ts); /auth/signup sits behind the authenticator and requires settings:users:manage; reserved usernames are refused on every creation path (NFKC, invisible characters and separators normalised). Runtime-verified 2026-09-24: ROOT, Root and rOOt on a non-system id resolve to no grants, and the compiled signup route carries the authenticator hook. Closed 2026-09-24. Gaps noted on the issue: no mixed-case regression test yet. ⚠️ Same class, new path raised as F-53 | Resolved | 4 | blitz#1559 |
Live queries: all open security issues · Finboard project · filter by severity from the posture cards.
Findings tagged "needs runtime confirmation" in their issues (F-16 enforcement) should be validated dynamically — this register is from a read-only review, not a penetration test (see F-24). F-42 reachability was confirmed by code trace on 2026-09-15 and no longer needs runtime validation. | F-52 | Static S3 access keys on AWS instead of the ECS task role — BLOBACCESSKEY/BLOBSECRETKEY are long-lived credentials held in Secrets Manager and injected into the container environment. The ECS task role issues short-lived, automatically-rotated credentials and puts no long-lived secret in the environment; the AWS SDK picks it up with no code (omit credentials and the default provider chain finds the container credentials endpoint). ⚠️ Not a drop-in: three of the five runtime S3 sites cannot use a task role as written — they use a client that does not resolve the container credentials provider, so a client change is needed first (see the table on the issue). Applies to the AWS tier only; Railway/RustFS keeps static keys by necessity. Follow-up to blitz#1497, which made endpoint/region/path-style configurable and deliberately left credentials alone | Open | 4 · 8 | blitz#1498 | | F-54 | Client-settable authorship and assignment fields (F-25 Tier 2): 47 write routes where a caller can set createdby on update, who wrote or uploaded content (commentby, remarkedby, uploadedby), or preparer/reviewer/approver and owner on configuration data. No path to a falsified approval or SoD bypass was found (config roles aren't read by approval logic; task templates enforce distinct roles at activation). The supporting-document/comment group is borderline P1. Guarded by lint:security R5 (baseline 47); closes when R5 = 0 | Open | 16 | blitz#1565 |
Revision history
| Version | Date | Author | Change |
|---|---|---|---|
| 1.0 | 2026-06-24 | Sekhar Prakash | Iteration-1 register (F-01–F-28). |
| 1.1 | 2026-07-10 | Sekhar Prakash | Re-verification; F-13 resolved. |
| 1.2 | 2026-07-18 | Sekhar Prakash | F-36 resolved; F-37 added. |
| 1.3 | 2026-08-06 | Sekhar Prakash | F-02 resolved; F-38 added. |
| 1.4 | 2026-08-26 | Sekhar Prakash | F-39–F-41 added. |
| 1.5 | 2026-08-27 | Sekhar Prakash | Standardised: added Status column; re-verification history moved to the Audit Log. |
| 1.6 | 2026-09-10 | Sekhar Prakash | Reconciled against live issue state: F-01, F-03, F-39, F-40 resolved (closed 2026-09-04/09-09); F-42 added (blitz#1460); P3 tier added to the severity scale; freshness contract stated. |
| 1.7 | 2026-09-10 | Sekhar Prakash | F-34/F-35 re-confirmed by gitleaks over both full histories; the GCP key proven to be the same credential in both repos; LinkedIn client id/secret recorded as a second exposed credential class. |
| 1.11 | 2026-09-24 | Sekhar Prakash | F-37 Open → Partial. The row had drifted behind fin-infra#55 (closed 2026-09-09). The org ruleset blocks force-push, deletion and direct push on four repos; unreviewed merge (0 approvals, owner decision) and required status checks (removed 2026-09-15) remain open. |
| 1.10 | 2026-09-22 | Sekhar Prakash | Register↔issue reconciliation + full-codebase review. Compared all 47 rows against the 37 open security-labelled issues across six repos. ➕ F-48 (P0) — unauthenticated privilege escalation to full admin (blitz#1559); internet-reachable, and not among the CodeQL scan's 169 findings. ➕ F-49–F-51 (P1) — conversation IDOR cluster, token-design/revocation gaps, AWS OIDC :* trust wildcard (blitz#1560). 🔄 F-41 corrected Open → Resolved — LoginThrottleService is implemented and wired into /auth/login, and blitz#1352 has been CLOSED; the row had drifted behind the issue. ⚠️ F-02 annotated: the commit that closed it (c53f0c1f2, blitz#1033) is the same commit that introduced F-48 — recorded because it generalises, a Resolved entry is evidence about the old bug, not a clean bill of health for the code that fixed it. Clubbing decisions (no merges made): every candidate pair was checked and kept separate — F-06 (per-user rate limit on agents) vs F-41 (login lockout) are different surfaces and different fixes; F-21 (pre-commit secret scanning) vs F-31 (the five-lens CI programme) are different layers, and F-21's own evidence is about uncommitted local files. Deliberately NOT given F-numbers: fin-infra#280 and fin-infra#260 are remediation work, not weaknesses (#280 implements the fix for F-25/F-26); blitz#1026 and blitz#1388 are feature/architecture work carrying a security label. Rule applied — an F-number names a weakness in the product; an issue may name either a weakness or the work to fix one. ➕ F-52 — blitz#1498 (static S3 keys → ECS task role), a genuine hardening finding that predated this review and had never been numbered. ✅ F-number collision investigated and resolved: blitz#1388 claims "F-42" in its body, but that reference is stale and self-assigned — #1388 (2026-08-30) has never appeared in this register, while F-42 was allocated to blitz#1460 (2026-09-09) on 2026-09-10 in fin-internal-docs#87. The F-42 → #1460 mapping is correct and stands; no renumbering. The fix is a one-line correction to #1388's body. Reviewed and confirmed NOT findings: blitz#1026 is provisioning/bootstrap work (create the first tenant admin with a role) plus constant-hardcoding cleanup — ⚠️ it does flag that WELLKNOWN_TENANTS.DEFAULT_ID and WELLKNOWN_USERIDS.ROOT are the same all-zeros UUID, which is adjacent to F-48 and worth reading alongside it. blitz#1388 is an architecture decision (error-handling ADR) with a real security consequence (dev-mode detail leak, wrong status codes) but no specific exploitable weakness. Both keep their security label by owner decision (2026-09-22) — they carry genuine security consequences even though neither is a discrete weakness, so the label is doing real work as a triage signal. They therefore appear in the security dashboard count without a corresponding F-number, which is expected rather than drift. Still unmapped by design: blitz#1556 (Console check, residue of F-34/F-35). ⚠️ Scope: this review was static analysis only. Business-logic flaws, race conditions and runtime auth bypass still need F-24. |
| 1.11 | 2026-09-24 | Sekhar Prakash | F-25 re-measured by runtime scan (55 old routes fixed/disproved; 39 Tier-1 + 2 Rule-3 open) and lint:security R1/R5 moved to that scan. F-48 resolved (runtime-verified). ➕ F-53 (SSO binds by email → system identity, P1) and F-54 (F-25 Tier 2, P2). ⚠️ Not re-verified in this pass: F-26 and F-41 still read Open/P0 here while blitz#1015 and blitz#1352 are closed on GitHub. |
| 1.9 | 2026-09-22 | Sekhar Prakash | GitHub CodeQL + secret-scanning assessment triaged (org-wide free scan, 2026-09-21: 169 CodeQL findings + 2 secret hits; actions 71 · csharp 49 · javascript 49). 120 of 169 resolved or dispositioned, all JS/TS work merged to blitz main and verified by content. 🔴 F-35 CORRECTED — the "LinkedIn credential" does not exist: the 18 hits are the cash-remittance column names linkedinvoiceids/linkedinvoices matching on the substring linked+invoice; there is no LinkedIn integration in the codebase and no rotation was ever required. ✅ F-34/F-35 CLOSED — history rewrite declined (2,628 + 3,978 commits, 73 release tags invalidated, zero gain on a dead credential); blitz-ui's client-side exposure is structurally gone (GOOGLE_API_KEY is server-side only now, so API restriction — not referrer — is the remaining control, still unverified). ➕ F-43–F-47 added (P1) from the csharp half: unauthenticated ExcelRW controllers, cross-tenant object-key traversal, arbitrary assembly load from uploaded Razor templates, unparameterized DuckDB SQL, per-tenant SAP profile in logs. These chain — F-43 gates F-44, and F-44 controls which template F-45 compiles. SAST tooling decision now has evidence: Semgrep OSS is not a CodeQL substitute — p/javascript ships 74 rules, has no rule for insecure randomness / log injection / ReDoS / weak hashing, its SSRF+path rules are Express-specific (this stack is Elysia), and a purpose-built vulnerable file returned 0 findings. ⚠️ The scan cannot be re-run until 2026-12-20 (90-day cooldown on the free assessment — a rate limit, not a licence gate), so it cannot verify these fixes; each was verified by reading merged main. |
| 1.8 | 2026-09-15 | Sekhar Prakash | P1 re-verification pass — all 12 open P1 security issues re-checked against code; none fixed. ✅ GCP key regeneration confirmed → F-34/F-35 Open → Partial (blobs still in history but the credential is dead; LinkedIn creds and new-key restrictions unconfirmed). ⚠️ F-30 REGRESSED 93→161 CVEs (2→4 critical); root cause is SCA still absent from CI. F-42 reachability CONFIRMED by code trace (tenant-writable → raw SQL), severity held at P1. F-07 corrected — the 2026-09-09 'tokens are no longer cookies' note was half the system; blitz-ui still writes them to JS-readable cookies. F-28 root cause widened — the app logs tokens itself (full refresh token at INFO, unredacted bearer), so removing ?token= does not close the leak. F-10 widened to three production TLS sites. Header version corrected 1.6→1.8 (it had drifted behind the 1.7 changelog entry). |
Finaisse Internal — Confidential. Not for external distribution.