@hilbras/keystone 2.6.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +350 -0
- package/README.md +72 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -11
- package/dist/index.js.map +1 -1
- package/dist/plugins/rateLimit.d.ts +10 -7
- package/dist/plugins/rateLimit.d.ts.map +1 -1
- package/dist/plugins/rateLimit.js +75 -36
- package/dist/plugins/rateLimit.js.map +1 -1
- package/dist/routes/admin/organizations.d.ts.map +1 -1
- package/dist/routes/admin/organizations.js +2 -0
- package/dist/routes/admin/organizations.js.map +1 -1
- package/dist/routes/admin/platform.d.ts.map +1 -1
- package/dist/routes/admin/platform.js +14 -1
- package/dist/routes/admin/platform.js.map +1 -1
- package/dist/routes/apiKeys.d.ts.map +1 -1
- package/dist/routes/apiKeys.js +15 -1
- package/dist/routes/apiKeys.js.map +1 -1
- package/dist/routes/auth.d.ts.map +1 -1
- package/dist/routes/auth.js +104 -3
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/emailVerification.d.ts.map +1 -1
- package/dist/routes/emailVerification.js +2 -0
- package/dist/routes/emailVerification.js.map +1 -1
- package/dist/routes/magicLinks.d.ts.map +1 -1
- package/dist/routes/magicLinks.js +2 -0
- package/dist/routes/magicLinks.js.map +1 -1
- package/dist/routes/oauth2.d.ts.map +1 -1
- package/dist/routes/oauth2.js +16 -2
- package/dist/routes/oauth2.js.map +1 -1
- package/dist/routes/password.d.ts.map +1 -1
- package/dist/routes/password.js +2 -0
- package/dist/routes/password.js.map +1 -1
- package/dist/routes/scim.d.ts.map +1 -1
- package/dist/routes/scim.js +2 -0
- package/dist/routes/scim.js.map +1 -1
- package/dist/routes/smsOtp.d.ts.map +1 -1
- package/dist/routes/smsOtp.js +4 -0
- package/dist/routes/smsOtp.js.map +1 -1
- package/dist/routes/totp.d.ts.map +1 -1
- package/dist/routes/totp.js +18 -1
- package/dist/routes/totp.js.map +1 -1
- package/dist/services/configuration/profiles.d.ts +26 -0
- package/dist/services/configuration/profiles.d.ts.map +1 -1
- package/dist/services/configuration/profiles.js +80 -1
- package/dist/services/configuration/profiles.js.map +1 -1
- package/dist/services/events/subscribers/auditLog.d.ts.map +1 -1
- package/dist/services/events/subscribers/auditLog.js +38 -3
- package/dist/services/events/subscribers/auditLog.js.map +1 -1
- package/dist/services/events/types.d.ts +3 -1
- package/dist/services/events/types.d.ts.map +1 -1
- package/dist/services/events/validate.d.ts +1 -0
- package/dist/services/events/validate.d.ts.map +1 -1
- package/dist/services/events/validate.js +5 -1
- package/dist/services/events/validate.js.map +1 -1
- package/dist/services/localRateLimit.d.ts +44 -0
- package/dist/services/localRateLimit.d.ts.map +1 -0
- package/dist/services/localRateLimit.js +86 -0
- package/dist/services/localRateLimit.js.map +1 -0
- package/dist/services/refreshTokenState.d.ts +5 -0
- package/dist/services/refreshTokenState.d.ts.map +1 -0
- package/dist/services/refreshTokenState.js +30 -0
- package/dist/services/refreshTokenState.js.map +1 -0
- package/dist/services/setup/token.d.ts +12 -0
- package/dist/services/setup/token.d.ts.map +1 -1
- package/dist/services/setup/token.js +27 -3
- package/dist/services/setup/token.js.map +1 -1
- package/dist/services/tokens.d.ts +1 -0
- package/dist/services/tokens.d.ts.map +1 -1
- package/dist/services/tokens.js +1 -1
- package/dist/services/tokens.js.map +1 -1
- package/dist/services/trustedProxies.d.ts +24 -0
- package/dist/services/trustedProxies.d.ts.map +1 -1
- package/dist/services/trustedProxies.js +19 -0
- package/dist/services/trustedProxies.js.map +1 -1
- package/dist/services/webhooks.d.ts +16 -0
- package/dist/services/webhooks.d.ts.map +1 -1
- package/dist/services/webhooks.js +48 -3
- package/dist/services/webhooks.js.map +1 -1
- package/dist/setup-server.js +47 -3
- package/dist/setup-server.js.map +1 -1
- package/docs/API-REVIEW.md +121 -0
- package/docs/API.md +457 -0
- package/docs/ARCHITECTURE.md +142 -0
- package/docs/CONTRIBUTING.md +61 -0
- package/docs/DEPLOYMENT.md +257 -0
- package/docs/INTEGRATION.md +336 -0
- package/docs/LOGIN_FORM_INTEGRATION.md +306 -0
- package/docs/MIGRATION-1.7.md +70 -0
- package/docs/MIGRATION-1.8.md +183 -0
- package/docs/MIGRATION-1.9.md +200 -0
- package/docs/MIGRATION-2.0.md +203 -0
- package/docs/MIGRATION-2.4.md +185 -0
- package/docs/PERFORMANCE.md +155 -0
- package/docs/RBAC.md +100 -0
- package/docs/RE-AUDIT.md +72 -0
- package/docs/README.md +54 -0
- package/docs/RELEASE-1.7.md +53 -0
- package/docs/ROADMAP.md +41 -0
- package/docs/SECURITY.md +143 -0
- package/docs/adrs/001-identity-connectors-as-adapters.md +27 -0
- package/docs/adrs/002-versioned-event-bus.md +30 -0
- package/docs/adrs/003-bullmq-for-background-work.md +20 -0
- package/docs/adrs/004-argon2id-password-hashing.md +19 -0
- package/docs/plans/KEYSTONE_IMPROVEMENT_PLAN.md +334 -0
- package/docs/plans/UI_SIMPLIFICATION_IMPROVEMENT_PLAN.md +271 -0
- package/docs/security/audit.md +82 -0
- package/docs/security/configuration.md +60 -0
- package/docs/security/enterprise-sso.md +193 -0
- package/docs/security/mtls.md +132 -0
- package/docs/security/proxy-security.md +128 -0
- package/docs/security/rate-limiting.md +79 -0
- package/docs/security/registry-exceptions.md +34 -0
- package/docs/security/registry.json +649 -0
- package/docs/security/registry.md +657 -0
- package/docs/security/scopes.md +45 -0
- package/docs/security/supply-chain.md +49 -0
- package/docs/security/trust-boundaries.md +111 -0
- package/package.json +15 -6
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
# Security regression registry
|
|
2
|
+
|
|
3
|
+
<!-- Generated by scripts/render-security-registry.mjs from docs/security/registry.json. Do not edit by hand. -->
|
|
4
|
+
|
|
5
|
+
Every vulnerability found in Keystone, the fix, the test that would fail without it, and where it is documented. The registry is validated by `npm run registry:check`, which fails if an entry names a test that does not exist, if a security suite is claimed by no entry, or if a mandatory attack class is uncovered.
|
|
6
|
+
|
|
7
|
+
**46 findings.**
|
|
8
|
+
|
|
9
|
+
| Severity | Count |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| critical | 6 |
|
|
12
|
+
| high | 26 |
|
|
13
|
+
| medium | 13 |
|
|
14
|
+
| low | 1 |
|
|
15
|
+
|
|
16
|
+
## Mandatory attack classes
|
|
17
|
+
|
|
18
|
+
The CI pipeline must exercise each of these. Every one is claimed by at least one entry above; the check is mechanical, not a convention.
|
|
19
|
+
|
|
20
|
+
| Attack class | Findings |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `privilege-escalation` | SEC-001, SEC-002, SEC-003, SEC-007, SEC-023, SEC-025, SEC-028, SEC-032 |
|
|
23
|
+
| `mfa-bypass` | SEC-004, SEC-005, SEC-015, SEC-034 |
|
|
24
|
+
| `tenant-isolation` | SEC-001, SEC-003, SEC-006 |
|
|
25
|
+
| `token-replay` | SEC-012, SEC-013, SEC-038 |
|
|
26
|
+
| `oauth-attacks` | SEC-016, SEC-017, SEC-018, SEC-019, SEC-020, SEC-021 |
|
|
27
|
+
| `scim-cross-tenant` | SEC-006 |
|
|
28
|
+
| `mtls-spoofing` | SEC-007, SEC-009, SEC-010 |
|
|
29
|
+
| `xff-spoofing` | SEC-008, SEC-009 |
|
|
30
|
+
| `api-scope-escalation` | SEC-023, SEC-024, SEC-025 |
|
|
31
|
+
| `secret-disclosure` | SEC-026, SEC-027, SEC-028, SEC-029, SEC-030, SEC-031 |
|
|
32
|
+
| `session-persistence` | SEC-014, SEC-015, SEC-031, SEC-038 |
|
|
33
|
+
| `password-reset-invalidation` | SEC-014 |
|
|
34
|
+
|
|
35
|
+
## Critical
|
|
36
|
+
|
|
37
|
+
### SEC-001 — Organization role escalation through self-assignment
|
|
38
|
+
|
|
39
|
+
*Fixed in v1.7.0. Component: `authorization`.*
|
|
40
|
+
|
|
41
|
+
**Issue.** A member could set their own organization role, so a `member` could promote themselves to `owner` and then administer the organization they were only supposed to belong to.
|
|
42
|
+
|
|
43
|
+
**Fix.** src/routes/admin/organizations.ts — role changes are owner-only and cannot target the caller's own membership
|
|
44
|
+
|
|
45
|
+
**Test.** `src/tests/security/authorization/authorization.test.ts`
|
|
46
|
+
|
|
47
|
+
**Documentation.** [docs/security/trust-boundaries.md](./trust-boundaries.md)
|
|
48
|
+
|
|
49
|
+
**Attack classes.** `privilege-escalation`, `tenant-isolation`
|
|
50
|
+
|
|
51
|
+
### SEC-002 — Platform-owner routes reachable without the owner role
|
|
52
|
+
|
|
53
|
+
*Fixed in v1.7.0. Component: `authorization`.*
|
|
54
|
+
|
|
55
|
+
**Issue.** Platform administration endpoints checked authentication but not the owner role, so any authenticated session could list and modify every user, organization and application in the installation.
|
|
56
|
+
|
|
57
|
+
**Fix.** src/routes/admin/helpers.ts — requireOwner() on every platform route
|
|
58
|
+
|
|
59
|
+
**Test.** `src/tests/security/authorization/authorization.test.ts`
|
|
60
|
+
|
|
61
|
+
**Documentation.** [docs/security/trust-boundaries.md](./trust-boundaries.md)
|
|
62
|
+
|
|
63
|
+
**Attack classes.** `privilege-escalation`
|
|
64
|
+
|
|
65
|
+
### SEC-004 — MFA could be enabled without ever being enforced
|
|
66
|
+
|
|
67
|
+
*Fixed in v1.8.0. Component: `mfa`.*
|
|
68
|
+
|
|
69
|
+
**Issue.** MFA was checked on the interactive login path only. Every other token-issuing path — refresh, OAuth token exchange, the token login flow — issued a full session without consulting the user's MFA state, so the control was present in the UI and absent everywhere else.
|
|
70
|
+
|
|
71
|
+
**Fix.** src/services/application/authentication.ts — a single token-issuance chokepoint that every flow passes through
|
|
72
|
+
|
|
73
|
+
**Test.** `src/tests/security/mfa/mfa.test.ts`
|
|
74
|
+
|
|
75
|
+
**Documentation.** [docs/security/trust-boundaries.md](./trust-boundaries.md)
|
|
76
|
+
|
|
77
|
+
**Attack classes.** `mfa-bypass`
|
|
78
|
+
|
|
79
|
+
### SEC-006 — SCIM credentials were not organization-scoped
|
|
80
|
+
|
|
81
|
+
*Fixed in v1.9.0. Component: `scim`.*
|
|
82
|
+
|
|
83
|
+
**Issue.** A SCIM token was accepted as a global credential, so a provisioning client for one organization could read and write users and groups in every organization in the installation.
|
|
84
|
+
|
|
85
|
+
**Fix.** src/services/scimCredentials.ts and src/routes/scim.ts — a credential resolves to exactly one organization
|
|
86
|
+
|
|
87
|
+
**Test.** `src/tests/security/scim/isolation.test.ts`
|
|
88
|
+
|
|
89
|
+
**Documentation.** [docs/security/trust-boundaries.md](./trust-boundaries.md)
|
|
90
|
+
|
|
91
|
+
**Attack classes.** `scim-cross-tenant`, `tenant-isolation`
|
|
92
|
+
|
|
93
|
+
### SEC-007 — x-service-account-id was a complete authentication bypass
|
|
94
|
+
|
|
95
|
+
*Fixed in v2.0.0. Component: `trust-boundary`.*
|
|
96
|
+
|
|
97
|
+
**Issue.** The mTLS plugin trusted a client-supplied `x-service-account-id` header as the caller's identity. Any client that could reach the service could assert any service account and act as it, with no certificate and no secret.
|
|
98
|
+
|
|
99
|
+
**Fix.** src/plugins/mtls.ts — identity is derived from the verified client certificate and bound to its fingerprint
|
|
100
|
+
|
|
101
|
+
**Test.** `src/tests/security/proxy/trust-boundary.test.ts`
|
|
102
|
+
|
|
103
|
+
**Documentation.** [docs/security/mtls.md](./mtls.md)
|
|
104
|
+
|
|
105
|
+
**Attack classes.** `mtls-spoofing`, `privilege-escalation`
|
|
106
|
+
|
|
107
|
+
### SEC-023 — A wildcard scope let any API key act as any service account
|
|
108
|
+
|
|
109
|
+
*Fixed in v2.6.0. Component: `api-keys`.*
|
|
110
|
+
|
|
111
|
+
**Issue.** The scope registry contained a `service_account` entry that any key carrying could use as a wildcard against every scope check, so a narrowly scoped key behaved as a fully privileged one.
|
|
112
|
+
|
|
113
|
+
**Fix.** src/services/scopes.ts — the wildcard is gone; every scope must be named
|
|
114
|
+
|
|
115
|
+
**Test.** `src/tests/security/api-keys/scopes-and-principals.test.ts`
|
|
116
|
+
|
|
117
|
+
**Documentation.** [docs/security/scopes.md](./scopes.md)
|
|
118
|
+
|
|
119
|
+
**Attack classes.** `api-scope-escalation`, `privilege-escalation`
|
|
120
|
+
|
|
121
|
+
## High
|
|
122
|
+
|
|
123
|
+
### SEC-003 — Tenant-unsafe workflow execution
|
|
124
|
+
|
|
125
|
+
*Fixed in v1.7.0. Component: `authorization`.*
|
|
126
|
+
|
|
127
|
+
**Issue.** Workflow lookup was keyed by identifier alone, so a request naming another organization's workflow run executed it and read its output.
|
|
128
|
+
|
|
129
|
+
**Fix.** src/services/workflows/engine.ts — every lookup is scoped to the owning organization
|
|
130
|
+
|
|
131
|
+
**Test.** `src/tests/security/authorization/authorization.test.ts`
|
|
132
|
+
|
|
133
|
+
**Documentation.** [docs/security/trust-boundaries.md](./trust-boundaries.md)
|
|
134
|
+
|
|
135
|
+
**Attack classes.** `tenant-isolation`, `privilege-escalation`
|
|
136
|
+
|
|
137
|
+
### SEC-005 — TOTP verification was not bound to the user
|
|
138
|
+
|
|
139
|
+
*Fixed in v1.8.0. Component: `mfa`.*
|
|
140
|
+
|
|
141
|
+
**Issue.** A TOTP code was checked against a resolved secret without confirming the code belonged to the account being authenticated, so a valid code for one account could satisfy verification for another.
|
|
142
|
+
|
|
143
|
+
**Fix.** src/services/totp.ts — verification is user-scoped
|
|
144
|
+
|
|
145
|
+
**Test.** `src/tests/security/mfa/mfa.test.ts`
|
|
146
|
+
|
|
147
|
+
**Documentation.** [docs/security/trust-boundaries.md](./trust-boundaries.md)
|
|
148
|
+
|
|
149
|
+
**Attack classes.** `mfa-bypass`
|
|
150
|
+
|
|
151
|
+
### SEC-008 — Unconditional x-forwarded-for trust escaped every rate limit
|
|
152
|
+
|
|
153
|
+
*Fixed in v2.0.0. Component: `proxy`.*
|
|
154
|
+
|
|
155
|
+
**Issue.** `trustProxy: true` accepted x-forwarded-for from any peer, so a client could present a fresh address on every request and receive an unlimited rate-limit budget on every endpoint, including login and MFA verification.
|
|
156
|
+
|
|
157
|
+
**Fix.** src/services/trustedProxies.ts — forwarding headers are honoured only from explicitly configured networks
|
|
158
|
+
|
|
159
|
+
**Test.** `src/tests/security/rate-limiting/abuse-prevention.test.ts`
|
|
160
|
+
|
|
161
|
+
**Documentation.** [docs/security/proxy-security.md](./proxy-security.md)
|
|
162
|
+
|
|
163
|
+
**Attack classes.** `xff-spoofing`
|
|
164
|
+
|
|
165
|
+
### SEC-009 — Client-supplied identity headers were not stripped
|
|
166
|
+
|
|
167
|
+
*Fixed in v2.0.0. Component: `trust-boundary`.*
|
|
168
|
+
|
|
169
|
+
**Issue.** Identity-bearing headers survived to route handlers even when the peer was untrusted, so a header-stripping gap in one plugin would be a bypass in another.
|
|
170
|
+
|
|
171
|
+
**Fix.** src/plugins/headerSanitization.ts — untrusted identity headers are removed before routing
|
|
172
|
+
|
|
173
|
+
**Test.** `src/tests/security/proxy/trust-boundary.test.ts`
|
|
174
|
+
|
|
175
|
+
**Documentation.** [docs/security/trust-boundaries.md](./trust-boundaries.md)
|
|
176
|
+
|
|
177
|
+
**Attack classes.** `xff-spoofing`, `mtls-spoofing`
|
|
178
|
+
|
|
179
|
+
### SEC-010 — A service account certificate was not bound to the account it claimed
|
|
180
|
+
|
|
181
|
+
*Fixed in v2.0.0. Component: `mtls`.*
|
|
182
|
+
|
|
183
|
+
**Issue.** Certificate authentication proved possession of a valid certificate but not that the certificate belonged to the service account being used, so any certificate issued by a trusted CA could impersonate any service account.
|
|
184
|
+
|
|
185
|
+
**Fix.** src/plugins/mtls.ts — the certificate fingerprint must match the stored binding
|
|
186
|
+
|
|
187
|
+
**Test.** `src/tests/security/proxy/trust-boundary.test.ts`
|
|
188
|
+
|
|
189
|
+
**Documentation.** [docs/security/mtls.md](./mtls.md)
|
|
190
|
+
|
|
191
|
+
**Attack classes.** `mtls-spoofing`
|
|
192
|
+
|
|
193
|
+
### SEC-011 — A production container shipped a bundled npm with eight high-severity CVEs
|
|
194
|
+
|
|
195
|
+
*Fixed in v2.1.0. Component: `supply-chain`.*
|
|
196
|
+
|
|
197
|
+
**Issue.** The production image included npm itself, which carried eight high advisories. `npm audit` and the OSV scanner both read the lockfile, not the image, so the finding was invisible to every configured gate.
|
|
198
|
+
|
|
199
|
+
**Fix.** Dockerfile — npm is removed from the runtime stage
|
|
200
|
+
|
|
201
|
+
**Test.** `.github/workflows/supply-chain.yml`
|
|
202
|
+
|
|
203
|
+
**Documentation.** [docs/security/supply-chain.md](./supply-chain.md)
|
|
204
|
+
|
|
205
|
+
### SEC-012 — Single-use credentials were readable and consumable more than once
|
|
206
|
+
|
|
207
|
+
*Fixed in v2.2.0. Component: `tokens`.*
|
|
208
|
+
|
|
209
|
+
**Issue.** Magic links, password reset tokens and SMS OTP codes were checked and then consumed as two separate steps. Concurrent requests could each pass the check before any of them consumed the credential, so a single emailed code produced many sessions — 50 concurrent requests yielded 50 successful logins.
|
|
210
|
+
|
|
211
|
+
**Fix.** src/services/singleUse.ts — a single atomic claim, so exactly one request can consume a credential
|
|
212
|
+
|
|
213
|
+
**Test.** `src/tests/security/tokens/single-use.test.ts`
|
|
214
|
+
|
|
215
|
+
**Documentation.** [docs/security/trust-boundaries.md](./trust-boundaries.md)
|
|
216
|
+
|
|
217
|
+
**Attack classes.** `token-replay`
|
|
218
|
+
|
|
219
|
+
### SEC-014 — Password reset did not revoke existing sessions
|
|
220
|
+
|
|
221
|
+
*Fixed in v2.3.0. Component: `sessions`.*
|
|
222
|
+
|
|
223
|
+
**Issue.** Resetting a password left every existing session and refresh token valid. An attacker who had obtained a session before the victim changed their password retained access indefinitely, which defeats the purpose of the reset.
|
|
224
|
+
|
|
225
|
+
**Fix.** src/services/sessionRevocation.ts — a password change evicts sessions, refresh tokens and pending challenges
|
|
226
|
+
|
|
227
|
+
**Test.** `src/tests/security/sessions/session-revocation.test.ts`
|
|
228
|
+
|
|
229
|
+
**Documentation.** [docs/security/trust-boundaries.md](./trust-boundaries.md)
|
|
230
|
+
|
|
231
|
+
**Attack classes.** `password-reset-invalidation`, `session-persistence`
|
|
232
|
+
|
|
233
|
+
### SEC-015 — Enabling MFA did not invalidate sessions issued without it
|
|
234
|
+
|
|
235
|
+
*Fixed in v2.3.0. Component: `sessions`.*
|
|
236
|
+
|
|
237
|
+
**Issue.** Turning on a second factor left pre-existing sessions untouched, so an attacker holding a session captured before enrolment kept full access after the user believed they had raised their security.
|
|
238
|
+
|
|
239
|
+
**Fix.** src/services/sessionRevocation.ts — revocation is centralized and applied on MFA enablement
|
|
240
|
+
|
|
241
|
+
**Test.** `src/tests/security/mfa/mfa.test.ts`
|
|
242
|
+
|
|
243
|
+
**Documentation.** [docs/security/trust-boundaries.md](./trust-boundaries.md)
|
|
244
|
+
|
|
245
|
+
**Attack classes.** `session-persistence`, `mfa-bypass`
|
|
246
|
+
|
|
247
|
+
### SEC-016 — The authorization_code grant never verified the client secret
|
|
248
|
+
|
|
249
|
+
*Fixed in v2.4.0. Component: `oauth`.*
|
|
250
|
+
|
|
251
|
+
**Issue.** `verifyClientSecret` was called on the other grants but not on the authorization code exchange, so a confidential client could redeem a code with only its public client_id. Latent rather than live — the code still required a matching redirect_uri and the code itself — but it removed a layer that the grant was specified to have.
|
|
252
|
+
|
|
253
|
+
**Fix.** src/routes/oauth2.ts — client authentication is mandatory for confidential clients on every grant
|
|
254
|
+
|
|
255
|
+
**Test.** `src/tests/security/oauth/oauth2-hardening.test.ts`
|
|
256
|
+
|
|
257
|
+
**Documentation.** [docs/MIGRATION-2.4.md](./docs/MIGRATION-2.4.md)
|
|
258
|
+
|
|
259
|
+
**Attack classes.** `oauth-attacks`
|
|
260
|
+
|
|
261
|
+
### SEC-017 — javascript: and data: were accepted as redirect URIs
|
|
262
|
+
|
|
263
|
+
*Fixed in v2.4.0. Component: `oauth`.*
|
|
264
|
+
|
|
265
|
+
**Issue.** Redirect URI validation checked structure but not scheme, so a client could register a `javascript:` URI. The authorization response carrying a code or token would then execute in the context of the authorization page, turning the redirect into script execution.
|
|
266
|
+
|
|
267
|
+
**Fix.** src/services/redirectUri.ts — only https, and http for loopback, are registrable
|
|
268
|
+
|
|
269
|
+
**Test.** `src/tests/security/oauth/oauth2-hardening.test.ts`
|
|
270
|
+
|
|
271
|
+
**Documentation.** [docs/MIGRATION-2.4.md](./docs/MIGRATION-2.4.md)
|
|
272
|
+
|
|
273
|
+
**Attack classes.** `oauth-attacks`
|
|
274
|
+
|
|
275
|
+
### SEC-019 — ID token algorithm was inferred and expiry was optional
|
|
276
|
+
|
|
277
|
+
*Fixed in v2.4.0. Component: `oidc`.*
|
|
278
|
+
|
|
279
|
+
**Issue.** The ID token's signing algorithm was read from the token's own header rather than from the connector's registered configuration, and neither `exp` nor `iat` was required. A token could therefore be verified under an algorithm the operator never configured, and one with no expiry at all was accepted.
|
|
280
|
+
|
|
281
|
+
**Fix.** src/services/connectors/oidc.ts — the algorithm comes from configuration, and exp and iat are required
|
|
282
|
+
|
|
283
|
+
**Test.** `src/tests/security/oauth/oauth2-hardening.test.ts`
|
|
284
|
+
|
|
285
|
+
**Documentation.** [docs/MIGRATION-2.4.md](./docs/MIGRATION-2.4.md)
|
|
286
|
+
|
|
287
|
+
**Attack classes.** `oauth-attacks`
|
|
288
|
+
|
|
289
|
+
### SEC-020 — The default Google connector discarded the OIDC nonce
|
|
290
|
+
|
|
291
|
+
*Fixed in v2.5.0. Component: `oidc`.*
|
|
292
|
+
|
|
293
|
+
**Issue.** `GoogleConnector.exchangeCode` dropped the nonce when calling the token endpoint, so a correct nonce was never sent and the response could not be checked. This defeated SEC-018 for the most commonly configured provider, which is why the fix in 2.4.0 was not sufficient on its own.
|
|
294
|
+
|
|
295
|
+
**Fix.** src/services/connectors/google.ts — the nonce is forwarded on exchange
|
|
296
|
+
|
|
297
|
+
**Test.** `src/tests/security/oidc/enterprise-sso.test.ts`
|
|
298
|
+
|
|
299
|
+
**Documentation.** [docs/security/enterprise-sso.md](./enterprise-sso.md)
|
|
300
|
+
|
|
301
|
+
**Attack classes.** `oauth-attacks`
|
|
302
|
+
|
|
303
|
+
### SEC-024 — Scope enforcement failed open for requests without a key
|
|
304
|
+
|
|
305
|
+
*Fixed in v2.6.0. Component: `api-keys`.*
|
|
306
|
+
|
|
307
|
+
**Issue.** `requireScopes` returned early when `request.apiKeyId` was absent, so any authenticated path that reached a scoped route without going through key authentication skipped the scope check entirely.
|
|
308
|
+
|
|
309
|
+
**Fix.** src/services/scopes.ts — enforcement fails closed and treats a missing key as no authority
|
|
310
|
+
|
|
311
|
+
**Test.** `src/tests/security/api-keys/scopes-and-principals.test.ts`
|
|
312
|
+
|
|
313
|
+
**Documentation.** [docs/security/scopes.md](./scopes.md)
|
|
314
|
+
|
|
315
|
+
**Attack classes.** `api-scope-escalation`
|
|
316
|
+
|
|
317
|
+
### SEC-025 — Human-only operations were available to machine principals
|
|
318
|
+
|
|
319
|
+
*Fixed in v2.6.0. Component: `service-accounts`.*
|
|
320
|
+
|
|
321
|
+
**Issue.** Profile and MFA management accepted any authenticated principal, so a service account key could read and change a person's profile and second-factor settings.
|
|
322
|
+
|
|
323
|
+
**Fix.** src/plugins/machinePrincipal.ts — requireHumanPrincipal() rejects machine principals, placed after authentication so it can see them
|
|
324
|
+
|
|
325
|
+
**Test.** `src/tests/security/api-keys/scopes-and-principals.test.ts`
|
|
326
|
+
|
|
327
|
+
**Documentation.** [docs/security/scopes.md](./scopes.md)
|
|
328
|
+
|
|
329
|
+
**Attack classes.** `api-scope-escalation`, `privilege-escalation`
|
|
330
|
+
|
|
331
|
+
### SEC-026 — Configuration redaction used a denylist that missed half the secrets
|
|
332
|
+
|
|
333
|
+
*Fixed in v2.7.0. Component: `configuration`.*
|
|
334
|
+
|
|
335
|
+
**Issue.** Redaction matched a list of known secret-looking key names. Measuring it against the real configuration surface, 12 of 24 secret-shaped keys were returned unredacted, including the signing keys — so the owner-only configuration endpoint disclosed the material used to sign tokens.
|
|
336
|
+
|
|
337
|
+
**Fix.** src/services/configuration/profiles.ts — an allowlist of exposable keys replaces the denylist
|
|
338
|
+
|
|
339
|
+
**Test.** `src/tests/security/configuration/secrets-config.test.ts`
|
|
340
|
+
|
|
341
|
+
**Documentation.** [docs/security/configuration.md](./configuration.md)
|
|
342
|
+
|
|
343
|
+
**Attack classes.** `secret-disclosure`
|
|
344
|
+
|
|
345
|
+
### SEC-027 — An empty CORS allowlist permitted every origin with credentials
|
|
346
|
+
|
|
347
|
+
*Fixed in v2.7.0. Component: `configuration`.*
|
|
348
|
+
|
|
349
|
+
**Issue.** An unset or empty `ALLOWED_ORIGINS` was treated as 'allow all', combined with credentialed requests. A misconfiguration therefore produced a wildcard CORS policy that browsers enforce, rather than a closed one.
|
|
350
|
+
|
|
351
|
+
**Fix.** src/services/trustedProxies.ts — isOriginAllowed() fails closed on an empty allowlist, shared with the tests rather than duplicated
|
|
352
|
+
|
|
353
|
+
**Test.** `src/tests/security/configuration/secrets-config.test.ts`
|
|
354
|
+
|
|
355
|
+
**Documentation.** [docs/security/configuration.md](./configuration.md)
|
|
356
|
+
|
|
357
|
+
**Attack classes.** `secret-disclosure`
|
|
358
|
+
|
|
359
|
+
### SEC-028 — The setup server allowed any origin with credentials
|
|
360
|
+
|
|
361
|
+
*Fixed in v2.7.0. Component: `configuration`.*
|
|
362
|
+
|
|
363
|
+
**Issue.** The first-run setup server was configured with `origin: true` and credentials enabled, so any page in the operator's browser could call it during setup and complete provisioning.
|
|
364
|
+
|
|
365
|
+
**Fix.** src/routes/setup.ts — the setup origin is restricted to the server's own address
|
|
366
|
+
|
|
367
|
+
**Test.** `src/tests/security/configuration/secrets-config.test.ts`
|
|
368
|
+
|
|
369
|
+
**Documentation.** [docs/security/configuration.md](./configuration.md)
|
|
370
|
+
|
|
371
|
+
**Attack classes.** `secret-disclosure`, `privilege-escalation`
|
|
372
|
+
|
|
373
|
+
### SEC-029 — The setup token was written to stdout
|
|
374
|
+
|
|
375
|
+
*Fixed in v2.7.0. Component: `configuration`.*
|
|
376
|
+
|
|
377
|
+
**Issue.** The token granting initial owner access was printed to the process log, so it reached log aggregation, container stdout capture and any log shipper — the credential intended to bootstrap trust was the one most widely distributed.
|
|
378
|
+
|
|
379
|
+
**Fix.** src/services/setup/token.ts — the token is delivered once, and printing requires an explicit opt-in
|
|
380
|
+
|
|
381
|
+
**Test.** `src/tests/security/configuration/secrets-config.test.ts`
|
|
382
|
+
|
|
383
|
+
**Documentation.** [docs/security/configuration.md](./configuration.md)
|
|
384
|
+
|
|
385
|
+
**Attack classes.** `secret-disclosure`
|
|
386
|
+
|
|
387
|
+
### SEC-031 — Session cookies were not Secure by default in production
|
|
388
|
+
|
|
389
|
+
*Fixed in v2.7.0. Component: `configuration`.*
|
|
390
|
+
|
|
391
|
+
**Issue.** `COOKIE_SECURE` defaulted to false, so a production deployment that did not set it explicitly issued session cookies over plaintext, exposing them to interception on any non-TLS path.
|
|
392
|
+
|
|
393
|
+
**Fix.** src/plugins/auth.ts — Secure defaults to true when NODE_ENV is production
|
|
394
|
+
|
|
395
|
+
**Test.** `src/tests/security/configuration/secrets-config.test.ts`
|
|
396
|
+
|
|
397
|
+
**Documentation.** [docs/security/configuration.md](./configuration.md)
|
|
398
|
+
|
|
399
|
+
**Attack classes.** `secret-disclosure`, `session-persistence`
|
|
400
|
+
|
|
401
|
+
### SEC-032 — The setup server listened on all interfaces
|
|
402
|
+
|
|
403
|
+
*Fixed in v2.7.0. Component: `configuration`.*
|
|
404
|
+
|
|
405
|
+
**Issue.** The setup server bound 0.0.0.0, so during first run the unauthenticated provisioning endpoint was reachable from the network rather than only from the operator's machine.
|
|
406
|
+
|
|
407
|
+
**Fix.** src/routes/setup.ts — loopback by default, overridable, and warns when exposed
|
|
408
|
+
|
|
409
|
+
**Test.** `src/tests/security/configuration/secrets-config.test.ts`
|
|
410
|
+
|
|
411
|
+
**Documentation.** [docs/security/configuration.md](./configuration.md)
|
|
412
|
+
|
|
413
|
+
**Attack classes.** `privilege-escalation`
|
|
414
|
+
|
|
415
|
+
### SEC-033 — A Redis outage removed rate limiting entirely
|
|
416
|
+
|
|
417
|
+
*Fixed in v2.8.0. Component: `rate-limiting`.*
|
|
418
|
+
|
|
419
|
+
**Issue.** Every limiter returned 'allowed' when Redis was unavailable, so during an outage login, MFA verification, OTP verification and the OAuth token exchange had no limit at all. An outage is precisely when unlimited attempts are worth having, because a burst of guessing no longer looks like a burst.
|
|
420
|
+
|
|
421
|
+
**Fix.** src/services/localRateLimit.ts — a bounded in-process budget, used only when Redis is unavailable
|
|
422
|
+
|
|
423
|
+
**Test.** `src/tests/security/rate-limiting/abuse-prevention.test.ts`
|
|
424
|
+
|
|
425
|
+
**Documentation.** [docs/security/rate-limiting.md](./rate-limiting.md)
|
|
426
|
+
|
|
427
|
+
### SEC-034 — MFA verification shared one budget across every user behind an address
|
|
428
|
+
|
|
429
|
+
*Fixed in v2.8.0. Component: `rate-limiting`.*
|
|
430
|
+
|
|
431
|
+
**Issue.** The rate-limit key included the submitted email, which the verification endpoint does not carry, so all 20 attempts were shared across every user at that address. An office behind a single NAT could have every legitimate second-factor login locked out by ordinary traffic.
|
|
432
|
+
|
|
433
|
+
**Fix.** src/routes/auth.ts — the key includes the challenge, which identifies one login attempt
|
|
434
|
+
|
|
435
|
+
**Test.** `src/tests/security/rate-limiting/abuse-prevention.test.ts`
|
|
436
|
+
|
|
437
|
+
**Documentation.** [docs/security/rate-limiting.md](./rate-limiting.md)
|
|
438
|
+
|
|
439
|
+
**Attack classes.** `mfa-bypass`
|
|
440
|
+
|
|
441
|
+
### SEC-042 — The test runner's directory glob was shallower than the test tree
|
|
442
|
+
|
|
443
|
+
*Fixed in v2.9.0. Component: `testing`.*
|
|
444
|
+
|
|
445
|
+
**Issue.** `node --test` expands `**` as a single directory level rather than as globstar, so the discovery patterns stopped matching as soon as the security suites gained a directory level. 48 security tests — the entire authorization suite — stopped running, and the suite still reported success because the remaining files passed. A green run was reporting on less than the repository contained.
|
|
446
|
+
|
|
447
|
+
**Fix.** package.json — one pattern per directory depth, with no overlap
|
|
448
|
+
|
|
449
|
+
**Test.** `src/tests/security/registry.test.ts`
|
|
450
|
+
|
|
451
|
+
**Documentation.** [docs/security/registry.md](./registry.md)
|
|
452
|
+
|
|
453
|
+
### SEC-043 — SAML assertions were validated without a complete issuer and audience binding
|
|
454
|
+
|
|
455
|
+
*Fixed in v2.5.0. Component: `saml`.*
|
|
456
|
+
|
|
457
|
+
**Issue.** The response validator checked signature and conditions but did not require a complete binding between the assertion and the configured service-provider entity, so a valid signed assertion minted for a different relying party could be accepted here.
|
|
458
|
+
|
|
459
|
+
**Fix.** src/services/saml/validator.ts — the audience and issuer are both required and must match configuration
|
|
460
|
+
|
|
461
|
+
**Test.** `src/tests/security/saml/saml-validator.test.ts`
|
|
462
|
+
|
|
463
|
+
**Documentation.** [docs/security/enterprise-sso.md](./enterprise-sso.md)
|
|
464
|
+
|
|
465
|
+
### SEC-046 — Every service-account request produced no audit record at all
|
|
466
|
+
|
|
467
|
+
*Fixed in v2.9.0. Component: `audit`.*
|
|
468
|
+
|
|
469
|
+
**Issue.** A machine principal is represented in memory by a user object whose id is the sentinel `sa:<uuid>`, so that routes expecting `request.user` keep working without a matching user row. The audit subscriber passed that sentinel straight into `audit_log.user_id`, which is a uuid column. Postgres rejected the insert, the subscriber's catch logged `failed to write event` and the audit record was lost. The request itself succeeded, so nothing failed visibly, and the absence of a record looked exactly like a request that never happened. The practical effect was that every request authenticated by an API key or an mTLS service account left no audit trail — the privileged, non-human path, which is the one an attacker would most want to use quietly. Surfaced by a test whose fixture failed in CI and not locally, because CI's request ids and principal resolution differed enough for the bad insert to occur there.
|
|
470
|
+
|
|
471
|
+
**Fix.** src/services/events/subscribers/auditLog.ts — the sentinel is stripped, user_id is left null, and the service account is recorded in metadata
|
|
472
|
+
|
|
473
|
+
**Test.** `src/tests/security/service-accounts/audit-attribution.test.ts`
|
|
474
|
+
|
|
475
|
+
**Documentation.** [docs/security/audit.md](./audit.md)
|
|
476
|
+
|
|
477
|
+
## Medium
|
|
478
|
+
|
|
479
|
+
### SEC-013 — A used credential was not distinguishable from an unknown one
|
|
480
|
+
|
|
481
|
+
*Fixed in v2.2.0. Component: `tokens`.*
|
|
482
|
+
|
|
483
|
+
**Issue.** Replay of a consumed magic link, reset token or OTP returned the same error as presenting a credential that never existed, so the second use of a stolen code was invisible.
|
|
484
|
+
|
|
485
|
+
**Fix.** src/services/singleUse.ts — replay is detected and reported as its own outcome
|
|
486
|
+
|
|
487
|
+
**Test.** `src/tests/security/tokens/single-use.test.ts`
|
|
488
|
+
|
|
489
|
+
**Documentation.** [docs/security/trust-boundaries.md](./trust-boundaries.md)
|
|
490
|
+
|
|
491
|
+
**Attack classes.** `token-replay`
|
|
492
|
+
|
|
493
|
+
### SEC-018 — OIDC authorization requests carried no nonce
|
|
494
|
+
|
|
495
|
+
*Fixed in v2.4.0. Component: `oidc`.*
|
|
496
|
+
|
|
497
|
+
**Issue.** The authorization request generated no nonce and the ID token response was not checked for one, so a token minted for a different session could be replayed into this one.
|
|
498
|
+
|
|
499
|
+
**Fix.** src/routes/oauth2.ts — a nonce is generated per request and verified on the response
|
|
500
|
+
|
|
501
|
+
**Test.** `src/tests/security/oauth/oauth2-hardening.test.ts`
|
|
502
|
+
|
|
503
|
+
**Documentation.** [docs/MIGRATION-2.4.md](./docs/MIGRATION-2.4.md)
|
|
504
|
+
|
|
505
|
+
**Attack classes.** `oauth-attacks`
|
|
506
|
+
|
|
507
|
+
### SEC-021 — An unsigned SAML Issuer element was not validated
|
|
508
|
+
|
|
509
|
+
*Fixed in v2.5.0. Component: `saml`.*
|
|
510
|
+
|
|
511
|
+
**Issue.** The Issuer was compared only when it fell inside a signed region. An attacker could place an unsigned Issuer naming a trusted IdP alongside a signed assertion from another IdP. Confirmed to lie outside both signed regions, so it was a real gap in the trust decision rather than a theoretical one.
|
|
512
|
+
|
|
513
|
+
**Fix.** src/services/saml/validator.ts — the Issuer is validated against the configured entity id in all cases
|
|
514
|
+
|
|
515
|
+
**Test.** `src/tests/security/saml/saml-adversarial.test.ts`
|
|
516
|
+
|
|
517
|
+
**Documentation.** [docs/security/enterprise-sso.md](./enterprise-sso.md)
|
|
518
|
+
|
|
519
|
+
**Attack classes.** `oauth-attacks`
|
|
520
|
+
|
|
521
|
+
### SEC-022 — RelayState verification threw instead of returning false
|
|
522
|
+
|
|
523
|
+
*Fixed in v2.5.0. Component: `saml`.*
|
|
524
|
+
|
|
525
|
+
**Issue.** A RelayState that failed its integrity check raised rather than returning a rejection, turning a routine validation failure into a 500 and obscuring the real cause.
|
|
526
|
+
|
|
527
|
+
**Fix.** src/services/saml/relayState.ts — verification returns a boolean and never throws
|
|
528
|
+
|
|
529
|
+
**Test.** `src/tests/security/saml/saml-adversarial.test.ts`
|
|
530
|
+
|
|
531
|
+
**Documentation.** [docs/security/enterprise-sso.md](./enterprise-sso.md)
|
|
532
|
+
|
|
533
|
+
### SEC-030 — Webhook signing secrets were stored in plaintext
|
|
534
|
+
|
|
535
|
+
*Fixed in v2.7.0. Component: `secrets`.*
|
|
536
|
+
|
|
537
|
+
**Issue.** Webhook secrets were stored as issued, so a database read, a backup or an admin query returned material that lets an attacker forge delivery attempts signed as this installation.
|
|
538
|
+
|
|
539
|
+
**Fix.** src/services/webhooks.ts — AES-256-GCM at rest, with legacy plaintext still readable
|
|
540
|
+
|
|
541
|
+
**Test.** `src/tests/security/configuration/secrets-config.test.ts`
|
|
542
|
+
|
|
543
|
+
**Documentation.** [docs/security/configuration.md](./configuration.md)
|
|
544
|
+
|
|
545
|
+
**Attack classes.** `secret-disclosure`
|
|
546
|
+
|
|
547
|
+
### SEC-035 — Credential spraying was unbounded
|
|
548
|
+
|
|
549
|
+
*Fixed in v2.8.0. Component: `rate-limiting`.*
|
|
550
|
+
|
|
551
|
+
**Issue.** The login budget was keyed on address and submitted address together, so it stopped repeated guesses at one account and did nothing about an attacker varying the address across a thousand accounts from one host.
|
|
552
|
+
|
|
553
|
+
**Fix.** src/routes/auth.ts — a second, address-keyed budget bounds spraying independently
|
|
554
|
+
|
|
555
|
+
**Test.** `src/tests/security/rate-limiting/abuse-prevention.test.ts`
|
|
556
|
+
|
|
557
|
+
**Documentation.** [docs/security/rate-limiting.md](./rate-limiting.md)
|
|
558
|
+
|
|
559
|
+
### SEC-036 — A refused request left no record
|
|
560
|
+
|
|
561
|
+
*Fixed in v2.8.0. Component: `audit`.*
|
|
562
|
+
|
|
563
|
+
**Issue.** A rate-limit trip produced a 429 and nothing else, so sustained guessing at login or MFA verification was invisible except in aggregate. The requests that most warranted attention were the only ones absent from the log.
|
|
564
|
+
|
|
565
|
+
**Fix.** src/plugins/rateLimit.ts — rate_limit_triggered records the endpoint, address and which limiter decided
|
|
566
|
+
|
|
567
|
+
**Test.** `src/tests/security/rate-limiting/abuse-prevention.test.ts`
|
|
568
|
+
|
|
569
|
+
**Documentation.** [docs/security/rate-limiting.md](./rate-limiting.md)
|
|
570
|
+
|
|
571
|
+
### SEC-037 — Failed logins were never audited
|
|
572
|
+
|
|
573
|
+
*Fixed in v2.8.0. Component: `audit`.*
|
|
574
|
+
|
|
575
|
+
**Issue.** user_login_failed existed in the event vocabulary and was never emitted, on either login route. Password guessing left no record.
|
|
576
|
+
|
|
577
|
+
**Fix.** src/routes/auth.ts — emitted on both routes, with no user id since the submitted address may match no account
|
|
578
|
+
|
|
579
|
+
**Test.** `src/tests/security/authentication/login-abuse.test.ts`
|
|
580
|
+
|
|
581
|
+
**Documentation.** [docs/security/audit.md](./audit.md)
|
|
582
|
+
|
|
583
|
+
### SEC-038 — A replayed refresh token was indistinguishable from an unknown one
|
|
584
|
+
|
|
585
|
+
*Fixed in v2.8.0. Component: `tokens`.*
|
|
586
|
+
|
|
587
|
+
**Issue.** Rotation consumes a refresh token, so a second presentation failed exactly as a token that never existed would. A leaked token used twice was therefore treated as a typo, and the account's other credentials stayed valid.
|
|
588
|
+
|
|
589
|
+
**Fix.** src/services/refreshTokenState.ts — a spent token is identified and the account's credentials are revoked
|
|
590
|
+
|
|
591
|
+
**Test.** `src/tests/security/authentication/login-abuse.test.ts`
|
|
592
|
+
|
|
593
|
+
**Documentation.** [docs/security/audit.md](./audit.md)
|
|
594
|
+
|
|
595
|
+
**Attack classes.** `token-replay`, `session-persistence`
|
|
596
|
+
|
|
597
|
+
### SEC-039 — API key creation had no rate limit
|
|
598
|
+
|
|
599
|
+
*Fixed in v2.8.0. Component: `api-keys`.*
|
|
600
|
+
|
|
601
|
+
**Issue.** Minting a credential is an authentication event but was unlimited, so a leaked session could enumerate a batch of keys.
|
|
602
|
+
|
|
603
|
+
**Fix.** src/routes/apiKeys.ts — bounded like any other authentication endpoint
|
|
604
|
+
|
|
605
|
+
**Test.** `src/tests/security/rate-limiting/abuse-prevention.test.ts`
|
|
606
|
+
|
|
607
|
+
**Documentation.** [docs/security/rate-limiting.md](./rate-limiting.md)
|
|
608
|
+
|
|
609
|
+
### SEC-040 — A stale build output kept deleted security tests running
|
|
610
|
+
|
|
611
|
+
*Fixed in v2.9.0. Component: `testing`.*
|
|
612
|
+
|
|
613
|
+
**Issue.** `tsc` does not remove output for sources that were renamed or deleted, so a moved suite ran twice under two paths and a deleted suite kept running. Test discovery therefore did not reflect the source tree, and a security test could be removed from the repository while continuing to pass in CI — or the reverse, a suite silently dropped out of the run.
|
|
614
|
+
|
|
615
|
+
**Fix.** package.json — the build cleans dist before compiling
|
|
616
|
+
|
|
617
|
+
**Test.** `scripts/verify-release-metadata.mjs`
|
|
618
|
+
|
|
619
|
+
**Documentation.** [docs/security/registry.md](./registry.md)
|
|
620
|
+
|
|
621
|
+
### SEC-044 — The SSO endpoint could be resolved through a name the operator did not register
|
|
622
|
+
|
|
623
|
+
*Fixed in v2.5.0. Component: `saml`.*
|
|
624
|
+
|
|
625
|
+
**Issue.** The endpoint that initiates SSO was addressable through a host alias that was never in the connection's registered endpoints, so a connection could be initiated against a name outside the configuration an operator reviewed.
|
|
626
|
+
|
|
627
|
+
**Fix.** src/routes/sso.ts — the request address must be one the connection registered
|
|
628
|
+
|
|
629
|
+
**Test.** `src/tests/security/saml/sso-endpoint.test.ts`
|
|
630
|
+
|
|
631
|
+
**Documentation.** [docs/security/enterprise-sso.md](./enterprise-sso.md)
|
|
632
|
+
|
|
633
|
+
### SEC-045 — The audit log export did not neutralise spreadsheet formula injection
|
|
634
|
+
|
|
635
|
+
*Fixed in v2.9.0. Component: `audit`.*
|
|
636
|
+
|
|
637
|
+
**Issue.** The CSV export quoted values containing a delimiter or a quote, but did not neutralise a value beginning with `=`, `+`, `-` or `@`. Several of the exported columns are attacker-supplied — the user agent above all — and a spreadsheet evaluates such a cell as a formula when the file is opened. An audit export is precisely the file an operator opens in a spreadsheet, so that is the expected consumer rather than an edge case. Found by Semgrep's `direct-response-write` rule during the triage for this release; the rule's own finding was a false positive, but the code it pointed at was not.
|
|
638
|
+
|
|
639
|
+
**Fix.** src/routes/admin/platform.ts — a value with a formula prefix in first position is prefixed with an apostrophe before quoting
|
|
640
|
+
|
|
641
|
+
**Test.** `src/tests/security/audit-export.test.ts`
|
|
642
|
+
|
|
643
|
+
**Documentation.** [docs/security/audit.md](./audit.md)
|
|
644
|
+
|
|
645
|
+
## Low
|
|
646
|
+
|
|
647
|
+
### SEC-041 — A test fixture path was correct at only one directory depth
|
|
648
|
+
|
|
649
|
+
*Fixed in v2.9.0. Component: `testing`.*
|
|
650
|
+
|
|
651
|
+
**Issue.** A suite reached its fixture by counting parent directories, which resolved correctly from `src` and not from the compiled output in `dist`. The suite passed and then failed to load the moment it was moved, which is a poor way to discover that a path is fragile.
|
|
652
|
+
|
|
653
|
+
**Fix.** src/tests/helpers/paths.ts — paths are anchored on the nearest package.json
|
|
654
|
+
|
|
655
|
+
**Test.** `src/tests/security/authorization/authorization.test.ts`
|
|
656
|
+
|
|
657
|
+
**Documentation.** [docs/security/registry.md](./registry.md)
|