@blamejs/exceptd-skills 0.16.14 → 0.16.16
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/AGENTS.md +3 -1
- package/CHANGELOG.md +8 -0
- package/README.md +5 -5
- package/bin/exceptd.js +3 -1
- package/data/_indexes/_meta.json +17 -15
- package/data/_indexes/activity-feed.json +16 -2
- package/data/_indexes/chains.json +7429 -451
- package/data/_indexes/currency.json +19 -1
- package/data/_indexes/frequency.json +135 -64
- package/data/_indexes/handoff-dag.json +9 -1
- package/data/_indexes/jurisdiction-map.json +11 -4
- package/data/_indexes/section-offsets.json +170 -0
- package/data/_indexes/stale-content.json +1 -1
- package/data/_indexes/summary-cards.json +77 -0
- package/data/_indexes/token-budget.json +103 -3
- package/data/_indexes/trigger-table.json +98 -1
- package/data/_indexes/xref.json +45 -4
- package/data/cwe-catalog.json +21 -5
- package/data/playbooks/cloud-iam-incident.json +26 -5
- package/data/playbooks/framework.json +2 -0
- package/data/playbooks/multitenancy-isolation.json +660 -0
- package/data/playbooks/sbom.json +21 -6
- package/data/playbooks/self-update-integrity.json +636 -0
- package/manifest-snapshot.json +106 -2
- package/manifest-snapshot.sha256 +1 -1
- package/manifest.json +160 -48
- package/package.json +2 -2
- package/sbom.cdx.json +92 -32
- package/skills/multitenancy-isolation/skill.md +83 -0
- package/skills/self-update-integrity/skill.md +79 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"id": "multitenancy-isolation",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"last_threat_review": "2026-06-02",
|
|
6
|
+
"threat_currency_score": 92,
|
|
7
|
+
"changelog": [
|
|
8
|
+
{
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"date": "2026-06-02",
|
|
11
|
+
"summary": "Initial seven-phase application-level multitenancy isolation + availability/DoS-resilience playbook. Covers two linked gaps that org access and availability controls do not operationalise: cross-tenant isolation (tenant id trusted from the client, queries not scoped at the data layer, row-level-security bypassable by the request role, un-namespaced cache/pub-sub/queue keys) and DoS-resilience (HTTP/2 Rapid Reset CVE-2023-44487 uncapped, no per-tenant rate/byte quota, unbounded per-request allocation, distributed locks without fencing/TTL, no circuit breaker on dependencies). Maps to ATT&CK T1078 / T1499 / T1499.001 / T1530 and CWE-639/770/863/668/400, and to NIST 800-53 AC-3 / SC-6, ISO 27001 A.8.31, NIS2 Art.21, and SOC 2 CC6."
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"owner": "@blamejs/platform-security",
|
|
15
|
+
"air_gap_mode": false,
|
|
16
|
+
"scope": "service",
|
|
17
|
+
"preconditions": [
|
|
18
|
+
{
|
|
19
|
+
"id": "app-source-or-config-read",
|
|
20
|
+
"description": "Agent must read the operator's application source and/or runtime configuration to inspect tenant resolution, data-layer isolation, shared-infra key namespacing, and availability controls. A host with neither marks the playbook visibility_gap=no_app_isolation_inventory.",
|
|
21
|
+
"check": "agent_has_filesystem_read == true OR agent_has_app_config == true",
|
|
22
|
+
"on_fail": "halt"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"mutex": [],
|
|
26
|
+
"feeds_into": [
|
|
27
|
+
{
|
|
28
|
+
"playbook_id": "cloud-iam-incident",
|
|
29
|
+
"condition": "finding.includes_cross_tenant_data_access == true"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"playbook_id": "framework",
|
|
33
|
+
"condition": "analyze.compliance_theater_check.verdict == 'theater'"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"domain": {
|
|
38
|
+
"name": "Application multitenancy isolation + availability/DoS resilience",
|
|
39
|
+
"attack_class": "api-abuse",
|
|
40
|
+
"atlas_refs": [],
|
|
41
|
+
"attack_refs": [
|
|
42
|
+
"T1078",
|
|
43
|
+
"T1499",
|
|
44
|
+
"T1499.001",
|
|
45
|
+
"T1530"
|
|
46
|
+
],
|
|
47
|
+
"cve_refs": [
|
|
48
|
+
"CVE-2023-44487"
|
|
49
|
+
],
|
|
50
|
+
"cwe_refs": [
|
|
51
|
+
"CWE-639",
|
|
52
|
+
"CWE-770",
|
|
53
|
+
"CWE-863",
|
|
54
|
+
"CWE-668",
|
|
55
|
+
"CWE-400"
|
|
56
|
+
],
|
|
57
|
+
"frameworks_in_scope": [
|
|
58
|
+
"nist-800-53",
|
|
59
|
+
"iso-27001-2022",
|
|
60
|
+
"nis2",
|
|
61
|
+
"soc2",
|
|
62
|
+
"uk-caf",
|
|
63
|
+
"au-ism"
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
"phases": {
|
|
67
|
+
"govern": {
|
|
68
|
+
"jurisdiction_obligations": [
|
|
69
|
+
{
|
|
70
|
+
"jurisdiction": "EU",
|
|
71
|
+
"regulation": "GDPR Art.33",
|
|
72
|
+
"obligation": "notify_regulator",
|
|
73
|
+
"window_hours": 72,
|
|
74
|
+
"clock_starts": "detect_confirmed",
|
|
75
|
+
"evidence_required": [
|
|
76
|
+
"cross_tenant_exposure_scope",
|
|
77
|
+
"affected_tenants_and_data_subjects"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"jurisdiction": "EU",
|
|
82
|
+
"regulation": "NIS2 Art.23",
|
|
83
|
+
"obligation": "notify_regulator",
|
|
84
|
+
"window_hours": 24,
|
|
85
|
+
"clock_starts": "detect_confirmed",
|
|
86
|
+
"evidence_required": [
|
|
87
|
+
"availability_impact_scope",
|
|
88
|
+
"tenant_isolation_failure_evidence"
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"theater_fingerprints": [
|
|
93
|
+
{
|
|
94
|
+
"pattern_id": "we-have-authz",
|
|
95
|
+
"claim": "We have an authorization layer, so tenants are isolated.",
|
|
96
|
+
"fast_detection_test": "Authorization existing is not data-layer isolation. Ask whether a query CAN run without a tenant predicate — if isolation depends on every code path remembering the filter, it is one missed query from a cross-tenant leak."
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"pattern_id": "rls-is-on",
|
|
100
|
+
"claim": "Row-level security is enabled.",
|
|
101
|
+
"fast_detection_test": "Ask which ROLE the request connection uses. RLS is bypassed by a superuser/owner/BYPASSRLS connection, so \"RLS enabled\" with an app connection on a bypass role is paper isolation."
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"pattern_id": "cloud-autoscales",
|
|
105
|
+
"claim": "The cloud autoscales, so we are DoS-resilient.",
|
|
106
|
+
"fast_detection_test": "Autoscaling pays the attacker's bill and does not stop an asymmetric DoS (Rapid Reset, unbounded allocation). Ask for per-tenant quotas, rapid-reset caps, and circuit breakers."
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"framework_context": {
|
|
110
|
+
"gap_summary": "Org controls treat \"we have authorization\" as tenant isolation and \"the cloud scales\" as DoS resilience. Neither requires data-layer tenant scoping (RLS under a non-bypass role), cross-tenant cache/queue namespacing, per-tenant quotas, HTTP/2 rapid-reset caps, or circuit breakers — the controls that actually contain a multitenant leak or an asymmetric DoS.",
|
|
111
|
+
"lag_score": 70,
|
|
112
|
+
"per_framework_gaps": [
|
|
113
|
+
{
|
|
114
|
+
"framework": "nist-800-53",
|
|
115
|
+
"control_id": "AC-3",
|
|
116
|
+
"designed_for": "access enforcement",
|
|
117
|
+
"insufficient_because": "satisfied by an authorization layer; does not require tenant scoping be structurally enforced at the data layer rather than per-query discipline."
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"framework": "nist-800-53",
|
|
121
|
+
"control_id": "SC-6",
|
|
122
|
+
"designed_for": "resource availability",
|
|
123
|
+
"insufficient_because": "named but not operationalised as per-tenant quotas, rapid-reset caps, or circuit breakers against asymmetric DoS."
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
"skill_preload": [
|
|
128
|
+
"multitenancy-isolation",
|
|
129
|
+
"webapp-security",
|
|
130
|
+
"cloud-security",
|
|
131
|
+
"framework-gap-analysis",
|
|
132
|
+
"compliance-theater",
|
|
133
|
+
"policy-exception-gen"
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
"direct": {
|
|
137
|
+
"threat_context": "Shared multitenant infrastructure has two linked failure classes. Isolation: if the tenant id is trusted from the client or the tenant filter lives in application discipline rather than the data layer, one authenticated tenant reads another's data (CWE-639 horizontal authz bypass) — and cache/queue keys leak the same way. Availability: asymmetric DoS (HTTP/2 Rapid Reset, unbounded per-request allocation) and the noisy-neighbour (no per-tenant quota) deny service for all tenants; autoscaling pays the bill without stopping it. These are application-posture gaps, not infra defaults.",
|
|
138
|
+
"rwep_threshold": {
|
|
139
|
+
"escalate": 55,
|
|
140
|
+
"monitor": 35,
|
|
141
|
+
"close": 20
|
|
142
|
+
},
|
|
143
|
+
"framework_lag_declaration": "A clean \"we have authorization and the cloud autoscales\" audit is NON-EVIDENCE for multitenancy isolation or DoS resilience; it confirms an auth layer and elastic infra, not data-layer RLS under a non-bypass role, cross-tenant namespacing, per-tenant quotas, rapid-reset caps, or circuit breakers.",
|
|
144
|
+
"skill_chain": [
|
|
145
|
+
{
|
|
146
|
+
"skill": "multitenancy-isolation",
|
|
147
|
+
"purpose": "enumerate the tenant-isolation + availability-resilience checks"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"skill": "webapp-security",
|
|
151
|
+
"purpose": "cross-reference the OWASP broken-object-level-authorization (BOLA) and resource-consumption classes"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"skill": "cloud-security",
|
|
155
|
+
"purpose": "frame the shared-infrastructure and autoscaling-economics dimension"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"skill": "framework-gap-analysis",
|
|
159
|
+
"purpose": "map findings to the AC-3 / SC-6 gaps the controls do not cover"
|
|
160
|
+
}
|
|
161
|
+
],
|
|
162
|
+
"token_budget": {
|
|
163
|
+
"estimated_total": 13000,
|
|
164
|
+
"breakdown": {
|
|
165
|
+
"govern": 1200,
|
|
166
|
+
"direct": 800,
|
|
167
|
+
"look": 4500,
|
|
168
|
+
"detect": 4000,
|
|
169
|
+
"analyze": 1800,
|
|
170
|
+
"validate": 500,
|
|
171
|
+
"close": 200
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"look": {
|
|
176
|
+
"artifacts": [
|
|
177
|
+
{
|
|
178
|
+
"id": "tenant-context-config",
|
|
179
|
+
"type": "config_file",
|
|
180
|
+
"source": "grep for agent-tenant / tenant id / tenant_id / X-Tenant / claim / principal across the request-auth + tenant-resolution path.",
|
|
181
|
+
"description": "How the effective tenant id is derived and whether it is bound to the authenticated principal vs taken from the client.",
|
|
182
|
+
"required": true,
|
|
183
|
+
"air_gap_alternative": "Inspect the tenant-resolution source for the binding between request tenant id and the session principal."
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"id": "data-isolation-config",
|
|
187
|
+
"type": "config_file",
|
|
188
|
+
"source": "grep for db-declare-row-policy / db-role-context / row level security / RLS / tenant predicate / BYPASSRLS across the data layer.",
|
|
189
|
+
"description": "Whether tenant scoping is enforced at the data layer (RLS / mandatory predicate) and which role the request connection uses.",
|
|
190
|
+
"required": true,
|
|
191
|
+
"air_gap_alternative": "Inspect the data-layer source for RLS policy declaration and the request-connection role."
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"id": "shared-infra-namespacing",
|
|
195
|
+
"type": "config_file",
|
|
196
|
+
"source": "grep for cache / pubsub / queue / cluster key/topic construction and whether the tenant id is included.",
|
|
197
|
+
"description": "Whether cache, pub/sub, and queue keys are namespaced by tenant to prevent cross-tenant collision.",
|
|
198
|
+
"required": false,
|
|
199
|
+
"air_gap_alternative": "Inspect the key/topic construction for a tenant component."
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"id": "availability-controls",
|
|
203
|
+
"type": "config_file",
|
|
204
|
+
"source": "grep for http2-teardown / RST_STREAM / tenant-quota / network-byte-quota / mail-server-rate-limit / circuit-breaker / resource-access-lock across the resilience layer.",
|
|
205
|
+
"description": "HTTP/2 rapid-reset capping, per-tenant rate/byte quotas, distributed-lock fencing, and circuit breakers on dependencies.",
|
|
206
|
+
"required": true,
|
|
207
|
+
"air_gap_alternative": "Inspect the resilience-layer source for stream-reset accounting, quotas, lock fencing, and circuit breakers."
|
|
208
|
+
}
|
|
209
|
+
],
|
|
210
|
+
"collection_scope": {
|
|
211
|
+
"time_window": "current application + infrastructure configuration and source state (point-in-time posture audit)",
|
|
212
|
+
"asset_scope": "every shared multitenant surface — data access, cache/pub-sub/queue, and the HTTP/2 + resource-quota + locking + circuit-breaker resilience layer"
|
|
213
|
+
},
|
|
214
|
+
"environment_assumptions": [
|
|
215
|
+
{
|
|
216
|
+
"assumption": "The application serves multiple tenants on shared infrastructure.",
|
|
217
|
+
"if_false": "A dedicated single-tenant-per-instance deployment removes the cross-tenant indicators; the availability/DoS indicators still apply."
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"assumption": "Application source or config is readable.",
|
|
221
|
+
"if_false": "Mark visibility_gap=no_app_isolation_inventory and report only what the request/data path reveals."
|
|
222
|
+
}
|
|
223
|
+
],
|
|
224
|
+
"fallback_if_unavailable": [
|
|
225
|
+
{
|
|
226
|
+
"artifact_id": "shared-infra-namespacing",
|
|
227
|
+
"fallback_action": "If no shared cache/pub-sub/queue is used, skip the key-collision indicator.",
|
|
228
|
+
"confidence_impact": "none"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"artifact_id": "availability-controls",
|
|
232
|
+
"fallback_action": "If availability is enforced wholly by an upstream CDN/WAF, inspect that policy instead of the origin defaults.",
|
|
233
|
+
"confidence_impact": "low"
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
},
|
|
237
|
+
"detect": {
|
|
238
|
+
"indicators": [
|
|
239
|
+
{
|
|
240
|
+
"id": "tenant-id-trusted-from-client",
|
|
241
|
+
"type": "config_value",
|
|
242
|
+
"value": "The tenant identifier used for data scoping is taken from a client-controlled header/parameter/JWT claim that is not bound, server-side, to the authenticated principal.",
|
|
243
|
+
"description": "If the client supplies its own tenant id and the server trusts it, an authenticated user of tenant A sets the id to tenant B and reads/writes B's data — horizontal cross-tenant authorization bypass.",
|
|
244
|
+
"confidence": "high",
|
|
245
|
+
"deterministic": false,
|
|
246
|
+
"attack_ref": "T1078",
|
|
247
|
+
"false_positive_checks_required": [
|
|
248
|
+
"Confirm the effective tenant id is derived from the authenticated session / verified token binding, not from a mutable request field — a tenant id read from a request header/param/body and used unverified is the finding.",
|
|
249
|
+
"A tenant id in a token that the server cryptographically verifies and cross-checks against the principal's tenancy is safe; the finding is an unverified client-supplied id."
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"id": "query-not-scoped-by-tenant",
|
|
254
|
+
"type": "config_value",
|
|
255
|
+
"value": "Data-access queries do not enforce a tenant predicate (no row-level security policy / no mandatory tenant filter), relying on application code to remember to scope each query.",
|
|
256
|
+
"description": "A single query that forgets the tenant filter returns or mutates other tenants' rows; relying on every code path to remember the filter is a when-not-if cross-tenant leak.",
|
|
257
|
+
"confidence": "high",
|
|
258
|
+
"deterministic": false,
|
|
259
|
+
"attack_ref": "T1078",
|
|
260
|
+
"false_positive_checks_required": [
|
|
261
|
+
"Confirm tenant scoping is enforced at the data layer (row-level security / a mandatory tenant predicate the ORM cannot omit) rather than per-query application discipline — queries that can run without a tenant predicate are the finding.",
|
|
262
|
+
"A single-tenant deployment, or a table with no tenant dimension, is not in scope."
|
|
263
|
+
]
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"id": "row-policy-bypassable-by-role-context",
|
|
267
|
+
"type": "config_value",
|
|
268
|
+
"value": "A row-level-security policy is declared but a query path runs under a role/connection that bypasses it (superuser/owner role, a BYPASSRLS connection, or a view that does not propagate the policy).",
|
|
269
|
+
"description": "A declared RLS policy that a privileged connection or unenforced view bypasses provides paper isolation; the bypass path reads across tenants despite the policy existing.",
|
|
270
|
+
"confidence": "high",
|
|
271
|
+
"deterministic": false,
|
|
272
|
+
"attack_ref": "T1078",
|
|
273
|
+
"false_positive_checks_required": [
|
|
274
|
+
"Confirm the application data connection runs under a role SUBJECT to RLS (not a bypass role) and that views/functions propagate the tenant policy — an app connection on a BYPASSRLS/owner role is the finding.",
|
|
275
|
+
"An out-of-band admin/migration connection that legitimately bypasses RLS is acceptable if it is not the request-serving path; confirm which connection serves user requests."
|
|
276
|
+
]
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"id": "cross-tenant-cache-or-queue-key-collision",
|
|
280
|
+
"type": "config_value",
|
|
281
|
+
"value": "Cache, pub/sub, or queue keys/topics are not namespaced by tenant, so one tenant's cached value, message, or job can be served to or processed for another.",
|
|
282
|
+
"description": "Un-namespaced shared-infrastructure keys cause cross-tenant data exposure (a cached response or queued payload crosses the boundary) — a leak through the cache/queue rather than the database.",
|
|
283
|
+
"confidence": "medium",
|
|
284
|
+
"deterministic": false,
|
|
285
|
+
"attack_ref": "T1530",
|
|
286
|
+
"false_positive_checks_required": [
|
|
287
|
+
"Confirm cache keys, pub/sub topics, and queue names include the tenant id in a collision-free way — shared keys derived only from the resource (no tenant component) are the finding.",
|
|
288
|
+
"A genuinely tenant-agnostic shared cache (public reference data with no tenant-specific content) is not in scope."
|
|
289
|
+
]
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"id": "http2-rapid-reset-uncapped",
|
|
293
|
+
"type": "config_value",
|
|
294
|
+
"value": "The HTTP/2 endpoint does not cap the rate of client-initiated stream resets (open-then-RST_STREAM), leaving it exposed to the Rapid Reset DDoS class (CVE-2023-44487).",
|
|
295
|
+
"description": "Rapid Reset lets a client open and immediately cancel streams at near-zero cost while the server does per-stream work, exhausting the server — a record-breaking application-layer DDoS primitive.",
|
|
296
|
+
"confidence": "high",
|
|
297
|
+
"deterministic": false,
|
|
298
|
+
"attack_ref": "T1499.001",
|
|
299
|
+
"false_positive_checks_required": [
|
|
300
|
+
"Confirm a per-connection cap on client-initiated stream resets (and connection teardown on abuse) is enforced — an HTTP/2 server below the rapid-reset-fixed release with no reset accounting is the finding.",
|
|
301
|
+
"An endpoint fronted by a CDN/WAF that absorbs rapid-reset upstream is mitigated; inspect the effective enforced control, not just the origin default."
|
|
302
|
+
]
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"id": "no-per-tenant-rate-or-byte-quota",
|
|
306
|
+
"type": "config_value",
|
|
307
|
+
"value": "There is no per-tenant (or per-IP) rate limit and no byte/connection quota, so a single tenant or client can exhaust shared CPU, bandwidth, or connection pools.",
|
|
308
|
+
"description": "Without per-tenant quotas one noisy or malicious tenant degrades or denies service for all others (the noisy-neighbour / resource-exhaustion DoS) on shared multitenant infrastructure.",
|
|
309
|
+
"confidence": "medium",
|
|
310
|
+
"deterministic": false,
|
|
311
|
+
"attack_ref": "T1499",
|
|
312
|
+
"false_positive_checks_required": [
|
|
313
|
+
"Confirm a per-tenant/per-IP rate limit AND a byte/connection quota are enforced on the shared surfaces (API, mail, network) — absence of either on a shared multitenant path is the finding.",
|
|
314
|
+
"A dedicated single-tenant instance with no resource sharing does not need per-tenant quotas; the finding is a shared surface without them."
|
|
315
|
+
]
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"id": "unbounded-resource-allocation-per-request",
|
|
319
|
+
"type": "config_value",
|
|
320
|
+
"value": "A request can drive unbounded allocation — no cap on result-set size, upload/body size, concurrent connections, or worker/goroutine fan-out.",
|
|
321
|
+
"description": "An unbounded per-request allocation lets a single crafted request consume memory/connections until the service fails — a cheap, single-request denial of service.",
|
|
322
|
+
"confidence": "medium",
|
|
323
|
+
"deterministic": false,
|
|
324
|
+
"attack_ref": "T1499.001",
|
|
325
|
+
"false_positive_checks_required": [
|
|
326
|
+
"Confirm caps on result-set / body size, concurrent connections, and fan-out are enforced — an allocation that scales with attacker-controlled input without a ceiling is the finding.",
|
|
327
|
+
"A bounded allocation already capped by config (max body size, pool limits) is safe; the finding is an uncapped path."
|
|
328
|
+
]
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"id": "distributed-lock-without-fencing",
|
|
332
|
+
"type": "config_value",
|
|
333
|
+
"value": "A distributed/resource-access lock has no fencing token or TTL, so a stale lock-holder (paused, GC, network-partitioned) can act after its lease expired, or a lock is never released.",
|
|
334
|
+
"description": "A lock without fencing/TTL causes either deadlock-style availability loss (never released) or split-brain writes (a stale holder acts after expiry) — a correctness-and-availability failure on shared state.",
|
|
335
|
+
"confidence": "low",
|
|
336
|
+
"deterministic": false,
|
|
337
|
+
"attack_ref": "T1499",
|
|
338
|
+
"false_positive_checks_required": [
|
|
339
|
+
"Confirm the lock carries a TTL AND a fencing token that the protected resource checks (rejecting writes from an expired lease) — a lock with neither is the finding.",
|
|
340
|
+
"A purely advisory in-process lock on non-shared state is not a distributed-lock finding; this targets cross-node resource locks."
|
|
341
|
+
]
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
"id": "no-circuit-breaker-on-dependency",
|
|
345
|
+
"type": "config_value",
|
|
346
|
+
"value": "Calls to a downstream dependency have no circuit breaker / bulkhead, so a slow or failing dependency cascades into connection-pool exhaustion and a wider outage.",
|
|
347
|
+
"description": "Without a circuit breaker, a single degraded dependency ties up all request workers waiting on it, turning a partial failure into a full availability outage (cascading failure).",
|
|
348
|
+
"confidence": "low",
|
|
349
|
+
"deterministic": false,
|
|
350
|
+
"attack_ref": "T1499",
|
|
351
|
+
"false_positive_checks_required": [
|
|
352
|
+
"Confirm downstream calls are wrapped in a circuit breaker / bulkhead with a timeout that sheds load when the dependency degrades — synchronous unbounded waits on a dependency are the finding.",
|
|
353
|
+
"A dependency that is genuinely non-critical and already isolated (async, best-effort) does not need a breaker; the finding is a critical synchronous dependency without one."
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
],
|
|
357
|
+
"false_positive_profile": [
|
|
358
|
+
{
|
|
359
|
+
"indicator_id": "tenant-id-trusted-from-client",
|
|
360
|
+
"benign_pattern": "Isolation/limits enforced at a lower layer (data-layer RLS, CDN/WAF rate limiting, dedicated single-tenant instance) or a surface with no shared-resource / cross-tenant dimension.",
|
|
361
|
+
"distinguishing_test": "Confirm the effective tenant id is derived from the authenticated session / verified token binding, not from a mutable request field — a tenant id read from a request header/param/body and used unverified is the finding."
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
"indicator_id": "query-not-scoped-by-tenant",
|
|
365
|
+
"benign_pattern": "Isolation/limits enforced at a lower layer (data-layer RLS, CDN/WAF rate limiting, dedicated single-tenant instance) or a surface with no shared-resource / cross-tenant dimension.",
|
|
366
|
+
"distinguishing_test": "Confirm tenant scoping is enforced at the data layer (row-level security / a mandatory tenant predicate the ORM cannot omit) rather than per-query application discipline — queries that can run without a tenant predicate are the finding."
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"indicator_id": "row-policy-bypassable-by-role-context",
|
|
370
|
+
"benign_pattern": "Isolation/limits enforced at a lower layer (data-layer RLS, CDN/WAF rate limiting, dedicated single-tenant instance) or a surface with no shared-resource / cross-tenant dimension.",
|
|
371
|
+
"distinguishing_test": "Confirm the application data connection runs under a role SUBJECT to RLS (not a bypass role) and that views/functions propagate the tenant policy — an app connection on a BYPASSRLS/owner role is the finding."
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"indicator_id": "cross-tenant-cache-or-queue-key-collision",
|
|
375
|
+
"benign_pattern": "Isolation/limits enforced at a lower layer (data-layer RLS, CDN/WAF rate limiting, dedicated single-tenant instance) or a surface with no shared-resource / cross-tenant dimension.",
|
|
376
|
+
"distinguishing_test": "Confirm cache keys, pub/sub topics, and queue names include the tenant id in a collision-free way — shared keys derived only from the resource (no tenant component) are the finding."
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"indicator_id": "http2-rapid-reset-uncapped",
|
|
380
|
+
"benign_pattern": "Isolation/limits enforced at a lower layer (data-layer RLS, CDN/WAF rate limiting, dedicated single-tenant instance) or a surface with no shared-resource / cross-tenant dimension.",
|
|
381
|
+
"distinguishing_test": "Confirm a per-connection cap on client-initiated stream resets (and connection teardown on abuse) is enforced — an HTTP/2 server below the rapid-reset-fixed release with no reset accounting is the finding."
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"indicator_id": "no-per-tenant-rate-or-byte-quota",
|
|
385
|
+
"benign_pattern": "Isolation/limits enforced at a lower layer (data-layer RLS, CDN/WAF rate limiting, dedicated single-tenant instance) or a surface with no shared-resource / cross-tenant dimension.",
|
|
386
|
+
"distinguishing_test": "Confirm a per-tenant/per-IP rate limit AND a byte/connection quota are enforced on the shared surfaces (API, mail, network) — absence of either on a shared multitenant path is the finding."
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"indicator_id": "unbounded-resource-allocation-per-request",
|
|
390
|
+
"benign_pattern": "Isolation/limits enforced at a lower layer (data-layer RLS, CDN/WAF rate limiting, dedicated single-tenant instance) or a surface with no shared-resource / cross-tenant dimension.",
|
|
391
|
+
"distinguishing_test": "Confirm caps on result-set / body size, concurrent connections, and fan-out are enforced — an allocation that scales with attacker-controlled input without a ceiling is the finding."
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"indicator_id": "distributed-lock-without-fencing",
|
|
395
|
+
"benign_pattern": "Isolation/limits enforced at a lower layer (data-layer RLS, CDN/WAF rate limiting, dedicated single-tenant instance) or a surface with no shared-resource / cross-tenant dimension.",
|
|
396
|
+
"distinguishing_test": "Confirm the lock carries a TTL AND a fencing token that the protected resource checks (rejecting writes from an expired lease) — a lock with neither is the finding."
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
"indicator_id": "no-circuit-breaker-on-dependency",
|
|
400
|
+
"benign_pattern": "Isolation/limits enforced at a lower layer (data-layer RLS, CDN/WAF rate limiting, dedicated single-tenant instance) or a surface with no shared-resource / cross-tenant dimension.",
|
|
401
|
+
"distinguishing_test": "Confirm downstream calls are wrapped in a circuit breaker / bulkhead with a timeout that sheds load when the dependency degrades — synchronous unbounded waits on a dependency are the finding."
|
|
402
|
+
}
|
|
403
|
+
],
|
|
404
|
+
"minimum_signal": {
|
|
405
|
+
"detected": "At least one isolation control is absent (client-trusted tenant id, query runnable without tenant predicate, RLS-bypassing request role, un-namespaced shared keys) or one availability control is absent (uncapped Rapid Reset, no per-tenant quota, unbounded allocation) on a production shared surface.",
|
|
406
|
+
"inconclusive": "A control is enforced at a layer the audit could not read (data-layer RLS confirmed only in migrations, CDN-enforced quotas) — record as visibility_gap, not a clean result.",
|
|
407
|
+
"not_detected": "Tenant id is bound to the principal, tenant scoping is data-layer-enforced under a non-bypass role, shared keys are tenant-namespaced, and the availability layer caps Rapid Reset, enforces per-tenant quotas, bounds allocation, fences locks, and circuit-breaks dependencies."
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
"analyze": {
|
|
411
|
+
"rwep_inputs": [
|
|
412
|
+
{
|
|
413
|
+
"signal_id": "tenant-id-trusted-from-client",
|
|
414
|
+
"rwep_factor": "blast_radius",
|
|
415
|
+
"weight": 25
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
"signal_id": "http2-rapid-reset-uncapped",
|
|
419
|
+
"rwep_factor": "public_poc",
|
|
420
|
+
"weight": 20
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
"signal_id": "query-not-scoped-by-tenant",
|
|
424
|
+
"rwep_factor": "active_exploitation",
|
|
425
|
+
"weight": 10
|
|
426
|
+
}
|
|
427
|
+
],
|
|
428
|
+
"blast_radius_model": {
|
|
429
|
+
"scope_question": "Does the gap expose all tenants' data, or deny service to all tenants, from a single authenticated user or request?",
|
|
430
|
+
"scoring_rubric": [
|
|
431
|
+
{
|
|
432
|
+
"condition": "A single authenticated tenant can read/write any other tenant's data (client-trusted id or unscoped query) on an internet-facing app",
|
|
433
|
+
"blast_radius_score": 5,
|
|
434
|
+
"description": "Full cross-tenant data compromise from one account"
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
"condition": "A single client can deny service to all tenants (uncapped Rapid Reset / no per-tenant quota)",
|
|
438
|
+
"blast_radius_score": 4,
|
|
439
|
+
"description": "Shared-service DoS affecting every tenant"
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
"condition": "Isolation/availability gap requires specific conditions or affects a limited surface",
|
|
443
|
+
"blast_radius_score": 2,
|
|
444
|
+
"description": "Constrained cross-tenant or availability impact"
|
|
445
|
+
}
|
|
446
|
+
]
|
|
447
|
+
},
|
|
448
|
+
"compliance_theater_check": {
|
|
449
|
+
"claim": "We have authorization and the cloud autoscales, so tenants are isolated and we are DoS-resilient.",
|
|
450
|
+
"audit_evidence": "An authorization middleware, an autoscaling group, a statement that RLS is enabled.",
|
|
451
|
+
"reality_test": "Probe whether a query runs without a tenant predicate, whether the request connection bypasses RLS, whether the tenant id is client-trusted, and whether Rapid Reset / unbounded allocation is capped. If a cross-tenant read or an asymmetric DoS succeeds, the auth layer and autoscaling did not isolate or protect.",
|
|
452
|
+
"theater_verdict_if_gap": "theater"
|
|
453
|
+
},
|
|
454
|
+
"framework_gap_mapping": [
|
|
455
|
+
{
|
|
456
|
+
"finding_id": "query-not-scoped-by-tenant",
|
|
457
|
+
"framework": "nist-800-53",
|
|
458
|
+
"claimed_control": "AC-3 (access enforcement)",
|
|
459
|
+
"actual_gap": "AC-3 is satisfied by an auth layer; it does not require tenant scoping be structurally enforced at the data layer."
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
"finding_id": "http2-rapid-reset-uncapped",
|
|
463
|
+
"framework": "nist-800-53",
|
|
464
|
+
"claimed_control": "SC-6 (resource availability)",
|
|
465
|
+
"actual_gap": "SC-6 is not operationalised as a rapid-reset cap against the CVE-2023-44487 asymmetric DDoS class."
|
|
466
|
+
}
|
|
467
|
+
],
|
|
468
|
+
"escalation_criteria": [
|
|
469
|
+
{
|
|
470
|
+
"condition": "A single authenticated user can access another tenant's data on a production app",
|
|
471
|
+
"action": "raise_severity"
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
"condition": "A single client can deny service to all tenants via an uncapped asymmetric DoS",
|
|
475
|
+
"action": "trigger_playbook"
|
|
476
|
+
}
|
|
477
|
+
]
|
|
478
|
+
},
|
|
479
|
+
"validate": {
|
|
480
|
+
"remediation_paths": [
|
|
481
|
+
{
|
|
482
|
+
"id": "bind-tenant-and-enforce-rls",
|
|
483
|
+
"description": "Derive the tenant id from the authenticated principal (never trust a client-supplied id), and enforce tenant scoping at the data layer via row-level security under a NON-bypass request role so a forgotten predicate cannot leak.",
|
|
484
|
+
"preconditions": [
|
|
485
|
+
"operator controls the data layer"
|
|
486
|
+
],
|
|
487
|
+
"priority": 1,
|
|
488
|
+
"for_signals": [
|
|
489
|
+
"tenant-id-trusted-from-client",
|
|
490
|
+
"query-not-scoped-by-tenant",
|
|
491
|
+
"row-policy-bypassable-by-role-context"
|
|
492
|
+
]
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
"id": "namespace-shared-keys",
|
|
496
|
+
"description": "Include the tenant id in every cache key, pub/sub topic, and queue name so cross-tenant collisions are impossible.",
|
|
497
|
+
"preconditions": [],
|
|
498
|
+
"priority": 2,
|
|
499
|
+
"for_signals": [
|
|
500
|
+
"cross-tenant-cache-or-queue-key-collision"
|
|
501
|
+
]
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
"id": "cap-rapid-reset-and-quota",
|
|
505
|
+
"description": "Cap client-initiated HTTP/2 stream resets per connection (CVE-2023-44487) and enforce per-tenant/per-IP rate + byte quotas on shared surfaces.",
|
|
506
|
+
"preconditions": [],
|
|
507
|
+
"priority": 1,
|
|
508
|
+
"for_signals": [
|
|
509
|
+
"http2-rapid-reset-uncapped",
|
|
510
|
+
"no-per-tenant-rate-or-byte-quota"
|
|
511
|
+
]
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
"id": "bound-allocation",
|
|
515
|
+
"description": "Cap result-set / body size, concurrent connections, and fan-out so no single request drives unbounded allocation.",
|
|
516
|
+
"preconditions": [],
|
|
517
|
+
"priority": 2,
|
|
518
|
+
"for_signals": [
|
|
519
|
+
"unbounded-resource-allocation-per-request"
|
|
520
|
+
]
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
"id": "fence-locks-and-circuit-break",
|
|
524
|
+
"description": "Give distributed locks a TTL + fencing token the resource checks, and wrap critical downstream dependencies in a circuit breaker / bulkhead with timeouts.",
|
|
525
|
+
"preconditions": [],
|
|
526
|
+
"priority": 3,
|
|
527
|
+
"for_signals": [
|
|
528
|
+
"distributed-lock-without-fencing",
|
|
529
|
+
"no-circuit-breaker-on-dependency"
|
|
530
|
+
]
|
|
531
|
+
}
|
|
532
|
+
],
|
|
533
|
+
"validation_tests": [
|
|
534
|
+
{
|
|
535
|
+
"id": "cross-tenant-read-blocked",
|
|
536
|
+
"test": "As an authenticated user of tenant A, set the tenant id to B (header/param/claim) and request B's data.",
|
|
537
|
+
"expected_result": "Request is denied / scoped to A — the server uses the principal-bound tenant, not the supplied id.",
|
|
538
|
+
"test_type": "negative"
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
"id": "unscoped-query-blocked",
|
|
542
|
+
"test": "Execute a request whose query omits the tenant predicate.",
|
|
543
|
+
"expected_result": "Data-layer RLS returns only the current tenant's rows (or the query is refused), not all tenants'.",
|
|
544
|
+
"test_type": "negative"
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
"id": "rapid-reset-capped",
|
|
548
|
+
"test": "Open and immediately RST_STREAM many HTTP/2 streams on one connection.",
|
|
549
|
+
"expected_result": "The connection is throttled/closed once the reset rate exceeds the cap; the server is not exhausted.",
|
|
550
|
+
"test_type": "negative"
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
"id": "legitimate-multitenant-flow",
|
|
554
|
+
"test": "Two tenants concurrently use the app normally within their quotas.",
|
|
555
|
+
"expected_result": "Each sees only its own data and gets fair service — no regression on the legitimate path.",
|
|
556
|
+
"test_type": "functional"
|
|
557
|
+
}
|
|
558
|
+
],
|
|
559
|
+
"residual_risk_statement": {
|
|
560
|
+
"risk": "A compromise of a tenant-administrator account, or of the principal-to-tenant mapping itself, still yields that tenant's scope (and any cross-tenant rights it legitimately holds).",
|
|
561
|
+
"why_remains": "Isolation enforces the principal's tenancy; it does not protect against compromise of a legitimately-scoped account, which is an identity-control concern.",
|
|
562
|
+
"acceptance_level": "ciso"
|
|
563
|
+
},
|
|
564
|
+
"evidence_requirements": [
|
|
565
|
+
{
|
|
566
|
+
"evidence_type": "config_diff",
|
|
567
|
+
"description": "The tenant-binding, data-layer RLS + role, shared-key namespacing, and availability-control (rapid-reset cap, quotas, locks, breakers) configuration as audited.",
|
|
568
|
+
"retention_period": "1 year"
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
"evidence_type": "exploit_replay_negative",
|
|
572
|
+
"description": "Outcomes of the cross-tenant-read / unscoped-query / rapid-reset negative tests plus the legitimate multitenant functional test.",
|
|
573
|
+
"retention_period": "1 year"
|
|
574
|
+
}
|
|
575
|
+
],
|
|
576
|
+
"regression_trigger": [
|
|
577
|
+
{
|
|
578
|
+
"condition": "A new shared resource, tenant-scoped table, or HTTP/2 endpoint is added",
|
|
579
|
+
"interval": "on change"
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
"condition": "Periodic re-attestation of multitenancy isolation + availability posture",
|
|
583
|
+
"interval": "quarterly"
|
|
584
|
+
}
|
|
585
|
+
]
|
|
586
|
+
},
|
|
587
|
+
"close": {
|
|
588
|
+
"evidence_package": {
|
|
589
|
+
"bundle_format": "csaf-2.0",
|
|
590
|
+
"contents": [
|
|
591
|
+
"app-isolation config snapshot",
|
|
592
|
+
"per-indicator findings + false-positive disposition",
|
|
593
|
+
"negative + functional test results",
|
|
594
|
+
"framework gap mapping"
|
|
595
|
+
],
|
|
596
|
+
"destination": ".exceptd/attestations/<session_id>/attestation.json"
|
|
597
|
+
},
|
|
598
|
+
"learning_loop": {
|
|
599
|
+
"enabled": true,
|
|
600
|
+
"lesson_template": {
|
|
601
|
+
"attack_vector": "Cross-tenant data access from a single authenticated account (client-trusted tenant id / unscoped query / RLS bypass / shared-key collision), or shared-service denial of service (Rapid Reset / no per-tenant quota / unbounded allocation).",
|
|
602
|
+
"control_gap": "Tenant scoping not data-layer-enforced under a non-bypass role; availability controls (rapid-reset cap, per-tenant quota, breaker) absent.",
|
|
603
|
+
"framework_gap": "NIST AC-3 + SC-6 + SOC 2 CC6 equate an auth layer with isolation and autoscaling with resilience.",
|
|
604
|
+
"new_control_requirement": "Tenant id MUST bind to the principal and scoping MUST be data-layer-enforced under a non-bypass role; shared surfaces MUST enforce per-tenant quotas and rapid-reset caps."
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
"notification_actions": [
|
|
608
|
+
{
|
|
609
|
+
"obligation_ref": "EU/GDPR Art.33 72h",
|
|
610
|
+
"deadline": "72h from detect_confirmed",
|
|
611
|
+
"recipient": "data protection authority",
|
|
612
|
+
"evidence_attached": [
|
|
613
|
+
"attestation"
|
|
614
|
+
]
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
"obligation_ref": "EU/NIS2 Art.23 24h",
|
|
618
|
+
"deadline": "24h from detect_confirmed",
|
|
619
|
+
"recipient": "national CSIRT / competent authority",
|
|
620
|
+
"evidence_attached": [
|
|
621
|
+
"attestation"
|
|
622
|
+
]
|
|
623
|
+
}
|
|
624
|
+
],
|
|
625
|
+
"exception_generation": {
|
|
626
|
+
"trigger_condition": "A legacy table cannot adopt data-layer RLS immediately.",
|
|
627
|
+
"exception_template": {
|
|
628
|
+
"scope": "the specific table / endpoint",
|
|
629
|
+
"duration": "90 days",
|
|
630
|
+
"compensating_controls": [
|
|
631
|
+
"mandatory tenant predicate in a shared query helper",
|
|
632
|
+
"automated test asserting no query runs without a tenant filter",
|
|
633
|
+
"enhanced cross-tenant access logging"
|
|
634
|
+
],
|
|
635
|
+
"risk_acceptance_owner": "ciso"
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
"regression_schedule": {
|
|
639
|
+
"next_run": "quarterly or on any new shared resource / tenant-scoped table / endpoint",
|
|
640
|
+
"trigger": "change to the isolation or availability surfaces, or quarterly re-attestation"
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
"directives": [
|
|
645
|
+
{
|
|
646
|
+
"id": "all-multitenant-surfaces",
|
|
647
|
+
"title": "Inventory + isolation/availability-test every shared multitenant surface",
|
|
648
|
+
"applies_to": {
|
|
649
|
+
"always": true
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
"id": "cross-tenant-bola-sweep",
|
|
654
|
+
"title": "Targeted cross-tenant (BOLA) + asymmetric-DoS sweep on internet-facing multitenant APIs",
|
|
655
|
+
"applies_to": {
|
|
656
|
+
"attack_technique": "T1078"
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
]
|
|
660
|
+
}
|