@resq-systems/security 1.0.5 → 2.1.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/README.md +236 -33
- package/lib/controls/address.d.mts +142 -0
- package/lib/controls/address.d.mts.map +1 -0
- package/lib/controls/address.mjs +533 -0
- package/lib/controls/address.mjs.map +1 -0
- package/lib/controls/csrf.d.mts +91 -0
- package/lib/controls/csrf.d.mts.map +1 -0
- package/lib/controls/csrf.mjs +200 -0
- package/lib/controls/csrf.mjs.map +1 -0
- package/lib/controls/index.d.mts +8 -0
- package/lib/controls/index.mjs +8 -0
- package/lib/controls/origin.d.mts +95 -0
- package/lib/controls/origin.d.mts.map +1 -0
- package/lib/controls/origin.mjs +156 -0
- package/lib/controls/origin.mjs.map +1 -0
- package/lib/controls/payload.d.mts +84 -0
- package/lib/controls/payload.d.mts.map +1 -0
- package/lib/controls/payload.mjs +147 -0
- package/lib/controls/payload.mjs.map +1 -0
- package/lib/controls/query.d.mts +169 -0
- package/lib/controls/query.d.mts.map +1 -0
- package/lib/controls/query.mjs +386 -0
- package/lib/controls/query.mjs.map +1 -0
- package/lib/controls/redirect.d.mts +92 -0
- package/lib/controls/redirect.d.mts.map +1 -0
- package/lib/controls/redirect.mjs +110 -0
- package/lib/controls/redirect.mjs.map +1 -0
- package/lib/controls/upload.d.mts +108 -0
- package/lib/controls/upload.d.mts.map +1 -0
- package/lib/controls/upload.mjs +374 -0
- package/lib/controls/upload.mjs.map +1 -0
- package/lib/crypto.d.mts +18 -5
- package/lib/crypto.d.mts.map +1 -1
- package/lib/crypto.mjs +35 -24
- package/lib/crypto.mjs.map +1 -1
- package/lib/hash.d.mts +51 -6
- package/lib/hash.d.mts.map +1 -1
- package/lib/hash.mjs +51 -6
- package/lib/hash.mjs.map +1 -1
- package/lib/index.d.mts +17 -2
- package/lib/index.mjs +19 -2
- package/lib/paths.d.mts +92 -0
- package/lib/paths.d.mts.map +1 -0
- package/lib/paths.mjs +140 -0
- package/lib/paths.mjs.map +1 -0
- package/lib/sanitize.d.mts +137 -35
- package/lib/sanitize.d.mts.map +1 -1
- package/lib/sanitize.mjs +170 -46
- package/lib/sanitize.mjs.map +1 -1
- package/lib/threats/capec.generated.d.mts +59 -0
- package/lib/threats/capec.generated.d.mts.map +1 -0
- package/lib/threats/capec.generated.mjs +644 -0
- package/lib/threats/capec.generated.mjs.map +1 -0
- package/lib/threats/engine.d.mts +94 -0
- package/lib/threats/engine.d.mts.map +1 -0
- package/lib/threats/engine.mjs +167 -0
- package/lib/threats/engine.mjs.map +1 -0
- package/lib/threats/index.d.mts +11 -0
- package/lib/threats/index.mjs +11 -0
- package/lib/threats/rules/datastore.d.mts +13 -0
- package/lib/threats/rules/datastore.d.mts.map +1 -0
- package/lib/threats/rules/datastore.mjs +366 -0
- package/lib/threats/rules/datastore.mjs.map +1 -0
- package/lib/threats/rules/index.d.mts +54 -0
- package/lib/threats/rules/index.d.mts.map +1 -0
- package/lib/threats/rules/index.mjs +121 -0
- package/lib/threats/rules/index.mjs.map +1 -0
- package/lib/threats/rules/markup.d.mts +28 -0
- package/lib/threats/rules/markup.d.mts.map +1 -0
- package/lib/threats/rules/markup.mjs +373 -0
- package/lib/threats/rules/markup.mjs.map +1 -0
- package/lib/threats/rules/protocol.d.mts +49 -0
- package/lib/threats/rules/protocol.d.mts.map +1 -0
- package/lib/threats/rules/protocol.mjs +175 -0
- package/lib/threats/rules/protocol.mjs.map +1 -0
- package/lib/threats/rules/system.d.mts +19 -0
- package/lib/threats/rules/system.d.mts.map +1 -0
- package/lib/threats/rules/system.mjs +455 -0
- package/lib/threats/rules/system.mjs.map +1 -0
- package/lib/threats/rules/web.d.mts +26 -0
- package/lib/threats/rules/web.d.mts.map +1 -0
- package/lib/threats/rules/web.mjs +412 -0
- package/lib/threats/rules/web.mjs.map +1 -0
- package/lib/threats/scoring.d.mts +59 -0
- package/lib/threats/scoring.d.mts.map +1 -0
- package/lib/threats/scoring.mjs +111 -0
- package/lib/threats/scoring.mjs.map +1 -0
- package/lib/threats/types.d.mts +245 -0
- package/lib/threats/types.d.mts.map +1 -0
- package/lib/threats/types.mjs +52 -0
- package/lib/threats/types.mjs.map +1 -0
- package/lib/threats/variants.d.mts +57 -0
- package/lib/threats/variants.d.mts.map +1 -0
- package/lib/threats/variants.mjs +144 -0
- package/lib/threats/variants.mjs.map +1 -0
- package/lib/unicode/confusables.d.mts +82 -0
- package/lib/unicode/confusables.d.mts.map +1 -0
- package/lib/unicode/confusables.mjs +954 -0
- package/lib/unicode/confusables.mjs.map +1 -0
- package/lib/unicode/index.d.mts +126 -0
- package/lib/unicode/index.d.mts.map +1 -0
- package/lib/unicode/index.mjs +288 -0
- package/lib/unicode/index.mjs.map +1 -0
- package/lib/validators.d.mts +341 -164
- package/lib/validators.d.mts.map +1 -1
- package/lib/validators.mjs +519 -338
- package/lib/validators.mjs.map +1 -1
- package/package.json +35 -8
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { ALL_THREAT_CONTEXTS } from "../types.mjs";
|
|
2
|
+
//#region src/threats/rules/markup.ts
|
|
3
|
+
/**
|
|
4
|
+
* Copyright 2026 ResQ Systems, Inc.
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* @fileoverview Document- and prompt-level signatures: XML/XXE, server-side template
|
|
20
|
+
* and SSI injection, LLM prompt injection, plus the small set of universal rules that
|
|
21
|
+
* apply in every context because no legitimate input needs them.
|
|
22
|
+
*
|
|
23
|
+
* XXE and SSTI are configuration weaknesses, not filtering ones. XXE is prevented by
|
|
24
|
+
* disabling DTD processing and external entity resolution in the parser; SSTI is
|
|
25
|
+
* prevented by passing untrusted values as template *data* rather than splicing them
|
|
26
|
+
* into template *source*. Both rule groups exist to catch a payload that a
|
|
27
|
+
* misconfigured parser or a `renderString(userInput)` call would otherwise execute.
|
|
28
|
+
*
|
|
29
|
+
* @module @resq-systems/security/threats/rules/markup
|
|
30
|
+
*/
|
|
31
|
+
/** Repeated across every XML rule. */
|
|
32
|
+
const XML_CONTROL = "Disable DTD processing and external entity resolution in the XML parser (noent:false, resolveExternals:false)";
|
|
33
|
+
/** XML external entity and entity-expansion signatures. */
|
|
34
|
+
const XML_INJECTION_RULES = [
|
|
35
|
+
{
|
|
36
|
+
id: "XML-ENTITY-DECL-001",
|
|
37
|
+
type: "xml_injection",
|
|
38
|
+
contexts: ["xml"],
|
|
39
|
+
severity: "critical",
|
|
40
|
+
confidence: "high",
|
|
41
|
+
description: "Inline ENTITY declaration",
|
|
42
|
+
cwe: 611,
|
|
43
|
+
primaryControl: XML_CONTROL,
|
|
44
|
+
pattern: /<!ENTITY\b/i
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "XML-EXTERNAL-ID-001",
|
|
48
|
+
type: "xml_injection",
|
|
49
|
+
contexts: ["xml"],
|
|
50
|
+
severity: "critical",
|
|
51
|
+
confidence: "high",
|
|
52
|
+
description: "SYSTEM or PUBLIC external identifier",
|
|
53
|
+
cwe: 611,
|
|
54
|
+
primaryControl: XML_CONTROL,
|
|
55
|
+
pattern: /\b(?:SYSTEM|PUBLIC)\s+["']/
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "XML-DOCTYPE-001",
|
|
59
|
+
type: "xml_injection",
|
|
60
|
+
contexts: ["xml"],
|
|
61
|
+
severity: "high",
|
|
62
|
+
confidence: "high",
|
|
63
|
+
description: "DOCTYPE declaration in untrusted XML",
|
|
64
|
+
cwe: 611,
|
|
65
|
+
primaryControl: XML_CONTROL,
|
|
66
|
+
pattern: /<!DOCTYPE\b/i
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "XML-XINCLUDE-001",
|
|
70
|
+
type: "xml_injection",
|
|
71
|
+
contexts: ["xml"],
|
|
72
|
+
severity: "high",
|
|
73
|
+
confidence: "high",
|
|
74
|
+
description: "XInclude directive — an entity-free route to the same file read",
|
|
75
|
+
cwe: 611,
|
|
76
|
+
primaryControl: XML_CONTROL,
|
|
77
|
+
pattern: /<\s{0,8}xi:include\b|www\.w3\.org\/2001\/XInclude/i
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "XML-CDATA-001",
|
|
81
|
+
type: "xml_injection",
|
|
82
|
+
contexts: ["xml"],
|
|
83
|
+
severity: "low",
|
|
84
|
+
confidence: "low",
|
|
85
|
+
description: "CDATA section, used to smuggle markup past naive filters",
|
|
86
|
+
cwe: 91,
|
|
87
|
+
primaryControl: XML_CONTROL,
|
|
88
|
+
pattern: /<!\[CDATA\[/
|
|
89
|
+
}
|
|
90
|
+
];
|
|
91
|
+
/** Repeated across most template-injection rules. */
|
|
92
|
+
const SSTI_CONTROL = "Pass untrusted values as template data (context variables), never as template source; render with autoescaping on";
|
|
93
|
+
/** Server-side template injection and SSI directives. */
|
|
94
|
+
const TEMPLATE_INJECTION_RULES = [
|
|
95
|
+
{
|
|
96
|
+
id: "SSTI-SANDBOX-ESCAPE-001",
|
|
97
|
+
type: "template_injection",
|
|
98
|
+
contexts: ["template"],
|
|
99
|
+
severity: "critical",
|
|
100
|
+
confidence: "high",
|
|
101
|
+
description: "Python object-graph traversal used to escape a template sandbox",
|
|
102
|
+
cwe: 1336,
|
|
103
|
+
primaryControl: SSTI_CONTROL,
|
|
104
|
+
pattern: /\b__(?:class|globals|subclasses|mro|builtins|import|base|init|reduce)__\b/
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "SSTI-OGNL-001",
|
|
108
|
+
type: "template_injection",
|
|
109
|
+
contexts: ["template"],
|
|
110
|
+
severity: "critical",
|
|
111
|
+
confidence: "high",
|
|
112
|
+
description: "OGNL/EL expression reaching a runtime execution class",
|
|
113
|
+
cwe: 917,
|
|
114
|
+
primaryControl: SSTI_CONTROL,
|
|
115
|
+
pattern: /_memberAccess|\bRuntime\s{0,8}\.\s{0,8}getRuntime|\bProcessBuilder\b|@java\.lang\b/i
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "SSTI-MUSTACHE-DELIMITER-001",
|
|
119
|
+
type: "template_injection",
|
|
120
|
+
contexts: ["template"],
|
|
121
|
+
severity: "high",
|
|
122
|
+
confidence: "medium",
|
|
123
|
+
description: "Handlebars/Jinja/Twig expression delimiters",
|
|
124
|
+
cwe: 1336,
|
|
125
|
+
primaryControl: SSTI_CONTROL,
|
|
126
|
+
pattern: /\{\{[^{}]{0,200}\}\}/
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "SSTI-STATEMENT-DELIMITER-001",
|
|
130
|
+
type: "template_injection",
|
|
131
|
+
contexts: ["template"],
|
|
132
|
+
severity: "high",
|
|
133
|
+
confidence: "medium",
|
|
134
|
+
description: "Jinja/Twig statement delimiters",
|
|
135
|
+
cwe: 1336,
|
|
136
|
+
primaryControl: SSTI_CONTROL,
|
|
137
|
+
pattern: /\{%[^%]{0,200}%\}/
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "SSTI-EL-DELIMITER-001",
|
|
141
|
+
type: "template_injection",
|
|
142
|
+
contexts: ["template"],
|
|
143
|
+
severity: "high",
|
|
144
|
+
confidence: "medium",
|
|
145
|
+
description: "JSP EL, Freemarker, or JS template-literal interpolation",
|
|
146
|
+
cwe: 1336,
|
|
147
|
+
primaryControl: SSTI_CONTROL,
|
|
148
|
+
pattern: /[$#]\{[^{}]{0,200}\}/
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "SSI-DIRECTIVE-001",
|
|
152
|
+
type: "template_injection",
|
|
153
|
+
contexts: ["template", "html"],
|
|
154
|
+
severity: "critical",
|
|
155
|
+
confidence: "high",
|
|
156
|
+
description: "Server-side include directive",
|
|
157
|
+
cwe: 97,
|
|
158
|
+
primaryControl: "Disable server-side includes, or serve untrusted content from a path with SSI turned off",
|
|
159
|
+
pattern: /<!--#\s{0,8}(?:exec|include|echo|config|fsize|flastmod|printenv|set)\b/i
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "SSTI-SCRIPTLET-DELIMITER-001",
|
|
163
|
+
type: "template_injection",
|
|
164
|
+
contexts: ["template"],
|
|
165
|
+
severity: "high",
|
|
166
|
+
confidence: "medium",
|
|
167
|
+
description: "ERB / JSP / ASP / EJS scriptlet delimiters",
|
|
168
|
+
cwe: 1336,
|
|
169
|
+
primaryControl: SSTI_CONTROL,
|
|
170
|
+
pattern: /<%[=\-@#!]|<%[^%]{0,200}%>/
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: "SSTI-FREEMARKER-DIRECTIVE-001",
|
|
174
|
+
type: "template_injection",
|
|
175
|
+
contexts: ["template"],
|
|
176
|
+
severity: "high",
|
|
177
|
+
confidence: "high",
|
|
178
|
+
description: "FreeMarker directive or user-defined directive call",
|
|
179
|
+
cwe: 1336,
|
|
180
|
+
primaryControl: SSTI_CONTROL,
|
|
181
|
+
pattern: /<#\s{0,8}(?:assign|list|if|else|import|include|macro|function|setting|attempt|global|local|nested|recurse|switch|visit|compress|noparse|outputformat)\b|<@[\w.$]{1,64}\s{0,8}\(/i
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "SSTI-VELOCITY-DIRECTIVE-001",
|
|
185
|
+
type: "template_injection",
|
|
186
|
+
contexts: ["template"],
|
|
187
|
+
severity: "high",
|
|
188
|
+
confidence: "medium",
|
|
189
|
+
description: "Velocity directive",
|
|
190
|
+
cwe: 1336,
|
|
191
|
+
primaryControl: SSTI_CONTROL,
|
|
192
|
+
pattern: /#(?:set|parse|evaluate|macro|foreach|include)\s{0,8}\(/i
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: "SSTI-DOTNET-REFLECTION-001",
|
|
196
|
+
type: "template_injection",
|
|
197
|
+
contexts: ["template"],
|
|
198
|
+
severity: "critical",
|
|
199
|
+
confidence: "high",
|
|
200
|
+
description: "Razor/.NET expression reaching a process or assembly-loading class",
|
|
201
|
+
cwe: 1336,
|
|
202
|
+
primaryControl: SSTI_CONTROL,
|
|
203
|
+
pattern: /\bSystem\.Diagnostics\.Process|\bProcess\.Start\s{0,8}\(|\bActivator\.CreateInstance\s{0,8}\(|\bAssembly\.Load\s{0,8}\(/i
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "SSTI-NODE-REQUIRE-001",
|
|
207
|
+
type: "template_injection",
|
|
208
|
+
contexts: ["template"],
|
|
209
|
+
severity: "critical",
|
|
210
|
+
confidence: "high",
|
|
211
|
+
description: "Node module-graph traversal reaching a process or filesystem module",
|
|
212
|
+
cwe: 1336,
|
|
213
|
+
primaryControl: SSTI_CONTROL,
|
|
214
|
+
pattern: /\bprocess\s{0,8}\.\s{0,8}(?:mainModule|binding|constructor)\b|\brequire\s{0,8}\(\s{0,8}["'](?:node:)?(?:child_process|fs|vm|os)["']/i
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: "SSTI-SMARTY-PHP-001",
|
|
218
|
+
type: "template_injection",
|
|
219
|
+
contexts: ["template"],
|
|
220
|
+
severity: "critical",
|
|
221
|
+
confidence: "high",
|
|
222
|
+
description: "Smarty {php} tag",
|
|
223
|
+
cwe: 94,
|
|
224
|
+
primaryControl: "Disable the {php} tag; it was removed entirely in Smarty 4",
|
|
225
|
+
pattern: /\{\s{0,8}\/?\s{0,8}php\s{0,8}\}/i
|
|
226
|
+
}
|
|
227
|
+
];
|
|
228
|
+
/** Repeated across every prompt-injection rule. */
|
|
229
|
+
const PROMPT_CONTROL = "Keep untrusted content in a clearly delimited data channel, constrain tool scopes, and require confirmation for consequential tool calls";
|
|
230
|
+
/**
|
|
231
|
+
* LLM prompt injection.
|
|
232
|
+
*
|
|
233
|
+
* These are the weakest signatures in the catalog by construction — prompt injection
|
|
234
|
+
* is expressed in natural language, so no finite pattern set bounds it. They are
|
|
235
|
+
* graded accordingly (mostly `medium` confidence) and are useful as telemetry and as
|
|
236
|
+
* a tripwire for human review, never as an authorization decision.
|
|
237
|
+
*/
|
|
238
|
+
const PROMPT_INJECTION_RULES = [
|
|
239
|
+
{
|
|
240
|
+
id: "PROMPT-OVERRIDE-001",
|
|
241
|
+
type: "prompt_injection",
|
|
242
|
+
contexts: ["llm_prompt"],
|
|
243
|
+
severity: "high",
|
|
244
|
+
confidence: "medium",
|
|
245
|
+
description: "Instruction to disregard prior directives",
|
|
246
|
+
cwe: 1427,
|
|
247
|
+
primaryControl: PROMPT_CONTROL,
|
|
248
|
+
pattern: /\b(?:ignore|disregard|forget|override|bypass)\s+(?:all\s+|any\s+|the\s+|your\s+|previous\s+|prior\s+|earlier\s+|above\s+){0,3}(?:instructions?|prompts?|rules?|directions?|guidelines?|constraints?)\b/i
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: "PROMPT-DELIMITER-SPOOF-001",
|
|
252
|
+
type: "prompt_injection",
|
|
253
|
+
contexts: ["llm_prompt"],
|
|
254
|
+
severity: "high",
|
|
255
|
+
confidence: "high",
|
|
256
|
+
description: "Chat-template control token forged in user content",
|
|
257
|
+
cwe: 1427,
|
|
258
|
+
primaryControl: PROMPT_CONTROL,
|
|
259
|
+
pattern: /<\|(?:im_start|im_end|endoftext|system|user|assistant|channel)\|>/i
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: "PROMPT-ROLE-SPOOF-001",
|
|
263
|
+
type: "prompt_injection",
|
|
264
|
+
contexts: ["llm_prompt"],
|
|
265
|
+
severity: "medium",
|
|
266
|
+
confidence: "medium",
|
|
267
|
+
description: "Line beginning with a conversational role label",
|
|
268
|
+
cwe: 1427,
|
|
269
|
+
primaryControl: PROMPT_CONTROL,
|
|
270
|
+
pattern: /^\s{0,8}(?:system|assistant|developer|tool)\s{0,8}:/im
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: "PROMPT-EXFIL-001",
|
|
274
|
+
type: "prompt_injection",
|
|
275
|
+
contexts: ["llm_prompt"],
|
|
276
|
+
severity: "high",
|
|
277
|
+
confidence: "medium",
|
|
278
|
+
description: "Request to disclose the system prompt or hidden instructions",
|
|
279
|
+
cwe: 1427,
|
|
280
|
+
primaryControl: PROMPT_CONTROL,
|
|
281
|
+
pattern: /\b(?:reveal|repeat|print|output|show|display|summarize)\s+(?:me\s+)?(?:your|the)\s+(?:system\s+|initial\s+|original\s+|hidden\s+){0,2}(?:prompt|instructions?|rules?|directives?)\b/i
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: "PROMPT-TOOL-COERCION-001",
|
|
285
|
+
type: "prompt_injection",
|
|
286
|
+
contexts: ["llm_prompt"],
|
|
287
|
+
severity: "high",
|
|
288
|
+
confidence: "medium",
|
|
289
|
+
description: "Directive coercing a specific tool or function invocation",
|
|
290
|
+
cwe: 1427,
|
|
291
|
+
primaryControl: PROMPT_CONTROL,
|
|
292
|
+
pattern: /\b(?:you\s+must|always|immediately|be\s+sure\s+to)\s+(?:call|invoke|run|use|execute)\s+(?:the\s+)?[\w.-]{1,64}\s+(?:tool|function|command|api)\b/i
|
|
293
|
+
}
|
|
294
|
+
];
|
|
295
|
+
/**
|
|
296
|
+
* Rules that apply in every context, including `general_text`.
|
|
297
|
+
*
|
|
298
|
+
* The bar for membership is high: a rule belongs here only if there is no legitimate
|
|
299
|
+
* reason for the character to appear in *any* user-supplied value. Bidirectional
|
|
300
|
+
* overrides (the "Trojan Source" class, CVE-2021-42574), zero-width characters used
|
|
301
|
+
* to spoof identity, and C0 control characters all clear it. SQL keywords and `../`
|
|
302
|
+
* emphatically do not, which is why they are context-scoped instead.
|
|
303
|
+
*/
|
|
304
|
+
const UNIVERSAL_RULES = [
|
|
305
|
+
{
|
|
306
|
+
id: "UNICODE-BIDI-OVERRIDE-001",
|
|
307
|
+
type: "homoglyph",
|
|
308
|
+
contexts: ALL_THREAT_CONTEXTS,
|
|
309
|
+
severity: "critical",
|
|
310
|
+
confidence: "high",
|
|
311
|
+
description: "Bidirectional override character — reorders rendered text away from its logical order",
|
|
312
|
+
cwe: 451,
|
|
313
|
+
primaryControl: "Reject bidi controls outright, or render with them stripped",
|
|
314
|
+
pattern: /[\u202a-\u202e\u2066-\u2069]/
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
id: "UNICODE-INVISIBLE-001",
|
|
318
|
+
type: "homoglyph",
|
|
319
|
+
contexts: [
|
|
320
|
+
"general_text",
|
|
321
|
+
"html",
|
|
322
|
+
"identifier",
|
|
323
|
+
"object_merge",
|
|
324
|
+
"url",
|
|
325
|
+
"url_parameter",
|
|
326
|
+
"sql",
|
|
327
|
+
"nosql",
|
|
328
|
+
"shell",
|
|
329
|
+
"filesystem",
|
|
330
|
+
"ldap",
|
|
331
|
+
"xpath",
|
|
332
|
+
"xml",
|
|
333
|
+
"template",
|
|
334
|
+
"spreadsheet",
|
|
335
|
+
"jwt",
|
|
336
|
+
"http_header",
|
|
337
|
+
"log",
|
|
338
|
+
"llm_prompt"
|
|
339
|
+
],
|
|
340
|
+
severity: "medium",
|
|
341
|
+
confidence: "medium",
|
|
342
|
+
description: "Zero-width or invisible formatting character",
|
|
343
|
+
cwe: 1007,
|
|
344
|
+
primaryControl: "Strip default-ignorable code points before comparison or storage",
|
|
345
|
+
pattern: /[\u00ad\u200b-\u200f\u2060-\u2064\ufeff]/
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
id: "UNICODE-INVISIBLE-IDENTIFIER-001",
|
|
349
|
+
type: "homoglyph",
|
|
350
|
+
contexts: ["identifier"],
|
|
351
|
+
severity: "high",
|
|
352
|
+
confidence: "medium",
|
|
353
|
+
description: "Invisible formatting character in a protected identifier",
|
|
354
|
+
cwe: 1007,
|
|
355
|
+
primaryControl: "Strip default-ignorable code points, then compare UTS #39 skeletons at registration time",
|
|
356
|
+
pattern: /[\u00ad\u200b-\u200f\u2060-\u2064\ufeff]/
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
id: "CONTROL-CHAR-001",
|
|
360
|
+
type: "resource_abuse",
|
|
361
|
+
contexts: ALL_THREAT_CONTEXTS,
|
|
362
|
+
severity: "medium",
|
|
363
|
+
confidence: "medium",
|
|
364
|
+
description: "C0/C1 control character other than tab, CR, or LF",
|
|
365
|
+
cwe: 74,
|
|
366
|
+
primaryControl: "Reject or strip control characters at the trust boundary",
|
|
367
|
+
pattern: /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/
|
|
368
|
+
}
|
|
369
|
+
];
|
|
370
|
+
//#endregion
|
|
371
|
+
export { PROMPT_INJECTION_RULES, TEMPLATE_INJECTION_RULES, UNIVERSAL_RULES, XML_INJECTION_RULES };
|
|
372
|
+
|
|
373
|
+
//# sourceMappingURL=markup.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markup.mjs","names":[],"sources":["../../../src/threats/rules/markup.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Document- and prompt-level signatures: XML/XXE, server-side template\n * and SSI injection, LLM prompt injection, plus the small set of universal rules that\n * apply in every context because no legitimate input needs them.\n *\n * XXE and SSTI are configuration weaknesses, not filtering ones. XXE is prevented by\n * disabling DTD processing and external entity resolution in the parser; SSTI is\n * prevented by passing untrusted values as template *data* rather than splicing them\n * into template *source*. Both rule groups exist to catch a payload that a\n * misconfigured parser or a `renderString(userInput)` call would otherwise execute.\n *\n * @module @resq-systems/security/threats/rules/markup\n */\n\nimport { ALL_THREAT_CONTEXTS, type ThreatRule } from \"../types.js\";\n\n//#region XML / XXE\n\n/** Repeated across every XML rule. */\nconst XML_CONTROL =\n\t\"Disable DTD processing and external entity resolution in the XML parser (noent:false, resolveExternals:false)\";\n\n/** XML external entity and entity-expansion signatures. */\nexport const XML_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"XML-ENTITY-DECL-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Inline ENTITY declaration\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!ENTITY\\b/i,\n\t},\n\t{\n\t\tid: \"XML-EXTERNAL-ID-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"SYSTEM or PUBLIC external identifier\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /\\b(?:SYSTEM|PUBLIC)\\s+[\"']/,\n\t},\n\t{\n\t\tid: \"XML-DOCTYPE-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"DOCTYPE declaration in untrusted XML\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!DOCTYPE\\b/i,\n\t},\n\t{\n\t\tid: \"XML-XINCLUDE-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"XInclude directive — an entity-free route to the same file read\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<\\s{0,8}xi:include\\b|www\\.w3\\.org\\/2001\\/XInclude/i,\n\t},\n\t{\n\t\tid: \"XML-CDATA-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"low\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"CDATA section, used to smuggle markup past naive filters\",\n\t\tcwe: 91,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!\\[CDATA\\[/,\n\t},\n];\n\n//#endregion\n\n//#region Template injection\n\n/** Repeated across most template-injection rules. */\nconst SSTI_CONTROL =\n\t\"Pass untrusted values as template data (context variables), never as template source; render with autoescaping on\";\n\n/** Server-side template injection and SSI directives. */\nexport const TEMPLATE_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"SSTI-SANDBOX-ESCAPE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Python object-graph traversal used to escape a template sandbox\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /\\b__(?:class|globals|subclasses|mro|builtins|import|base|init|reduce)__\\b/,\n\t},\n\t{\n\t\tid: \"SSTI-OGNL-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"OGNL/EL expression reaching a runtime execution class\",\n\t\tcwe: 917,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /_memberAccess|\\bRuntime\\s{0,8}\\.\\s{0,8}getRuntime|\\bProcessBuilder\\b|@java\\.lang\\b/i,\n\t},\n\t{\n\t\tid: \"SSTI-MUSTACHE-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Handlebars/Jinja/Twig expression delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// The inner class excludes `{` as well as `}`. With only `}` excluded, an\n\t\t// input of 100 000 `{` characters makes the engine consume 200 of them at\n\t\t// every start position and then backtrack — ~40ms per scan, measured by\n\t\t// tests/regex-safety.test.ts. Excluding `{` makes the failure immediate, and\n\t\t// costs nothing: a template expression does not contain a bare `{`.\n\t\tpattern: /\\{\\{[^{}]{0,200}\\}\\}/,\n\t},\n\t{\n\t\tid: \"SSTI-STATEMENT-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Jinja/Twig statement delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /\\{%[^%]{0,200}%\\}/,\n\t},\n\t{\n\t\tid: \"SSTI-EL-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"JSP EL, Freemarker, or JS template-literal interpolation\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// `{` excluded from the inner class for the same reason as the rule above.\n\t\tpattern: /[$#]\\{[^{}]{0,200}\\}/,\n\t},\n\t{\n\t\tid: \"SSI-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\", \"html\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Server-side include directive\",\n\t\tcwe: 97,\n\t\tprimaryControl:\n\t\t\t\"Disable server-side includes, or serve untrusted content from a path with SSI turned off\",\n\t\tpattern: /<!--#\\s{0,8}(?:exec|include|echo|config|fsize|flastmod|printenv|set)\\b/i,\n\t},\n\t{\n\t\tid: \"SSTI-SCRIPTLET-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\t// `template` only. A scriptlet spliced into an already-rendered response does\n\t\t// not execute — only template *source* is an execution path. SSI-DIRECTIVE-001\n\t\t// is in both because Apache and nginx post-process served HTML; no equivalent\n\t\t// stage exists for ERB, JSP, ASP, or EJS.\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"ERB / JSP / ASP / EJS scriptlet delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /<%[=\\-@#!]|<%[^%]{0,200}%>/,\n\t},\n\t{\n\t\tid: \"SSTI-FREEMARKER-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"FreeMarker directive or user-defined directive call\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern:\n\t\t\t/<#\\s{0,8}(?:assign|list|if|else|import|include|macro|function|setting|attempt|global|local|nested|recurse|switch|visit|compress|noparse|outputformat)\\b|<@[\\w.$]{1,64}\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-VELOCITY-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Velocity directive\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /#(?:set|parse|evaluate|macro|foreach|include)\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-DOTNET-REFLECTION-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Razor/.NET expression reaching a process or assembly-loading class\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// A bare Razor `@(` / `@{` delimiter rule was rejected: two characters of common\n\t\t// punctuation that matched Objective-C literals and `@media` queries. This\n\t\t// targets capability reach instead, mirroring how SSTI-OGNL-001 handles Java.\n\t\t// `Process\\.Start` is required literally — the spaced form matched the sentence\n\t\t// \"Our deployment process. Start(ing) Monday\".\n\t\tpattern:\n\t\t\t/\\bSystem\\.Diagnostics\\.Process|\\bProcess\\.Start\\s{0,8}\\(|\\bActivator\\.CreateInstance\\s{0,8}\\(|\\bAssembly\\.Load\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-NODE-REQUIRE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Node module-graph traversal reaching a process or filesystem module\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern:\n\t\t\t/\\bprocess\\s{0,8}\\.\\s{0,8}(?:mainModule|binding|constructor)\\b|\\brequire\\s{0,8}\\(\\s{0,8}[\"'](?:node:)?(?:child_process|fs|vm|os)[\"']/i,\n\t},\n\t{\n\t\tid: \"SSTI-SMARTY-PHP-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Smarty {php} tag\",\n\t\tcwe: 94,\n\t\tprimaryControl: \"Disable the {php} tag; it was removed entirely in Smarty 4\",\n\t\tpattern: /\\{\\s{0,8}\\/?\\s{0,8}php\\s{0,8}\\}/i,\n\t},\n];\n\n//#endregion\n\n//#region Prompt injection\n\n/** Repeated across every prompt-injection rule. */\nconst PROMPT_CONTROL =\n\t\"Keep untrusted content in a clearly delimited data channel, constrain tool scopes, and require confirmation for consequential tool calls\";\n\n/**\n * LLM prompt injection.\n *\n * These are the weakest signatures in the catalog by construction — prompt injection\n * is expressed in natural language, so no finite pattern set bounds it. They are\n * graded accordingly (mostly `medium` confidence) and are useful as telemetry and as\n * a tripwire for human review, never as an authorization decision.\n */\nexport const PROMPT_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PROMPT-OVERRIDE-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Instruction to disregard prior directives\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:ignore|disregard|forget|override|bypass)\\s+(?:all\\s+|any\\s+|the\\s+|your\\s+|previous\\s+|prior\\s+|earlier\\s+|above\\s+){0,3}(?:instructions?|prompts?|rules?|directions?|guidelines?|constraints?)\\b/i,\n\t},\n\t{\n\t\tid: \"PROMPT-DELIMITER-SPOOF-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Chat-template control token forged in user content\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern: /<\\|(?:im_start|im_end|endoftext|system|user|assistant|channel)\\|>/i,\n\t},\n\t{\n\t\tid: \"PROMPT-ROLE-SPOOF-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Line beginning with a conversational role label\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern: /^\\s{0,8}(?:system|assistant|developer|tool)\\s{0,8}:/im,\n\t},\n\t{\n\t\tid: \"PROMPT-EXFIL-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Request to disclose the system prompt or hidden instructions\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:reveal|repeat|print|output|show|display|summarize)\\s+(?:me\\s+)?(?:your|the)\\s+(?:system\\s+|initial\\s+|original\\s+|hidden\\s+){0,2}(?:prompt|instructions?|rules?|directives?)\\b/i,\n\t},\n\t{\n\t\tid: \"PROMPT-TOOL-COERCION-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Directive coercing a specific tool or function invocation\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:you\\s+must|always|immediately|be\\s+sure\\s+to)\\s+(?:call|invoke|run|use|execute)\\s+(?:the\\s+)?[\\w.-]{1,64}\\s+(?:tool|function|command|api)\\b/i,\n\t},\n];\n\n//#endregion\n\n//#region Universal\n\n/**\n * Every sink. An invisible formatting character has no legitimate place in a value\n * bound to any of them.\n *\n * This list previously excluded `sql`, `shell`, `ldap` and the other machine-syntax\n * sinks, on the stated grounds that those \"get the stricter {@link UNIVERSAL_RULES}\n * bidi and control-character rules instead\". Mutation testing showed that reasoning was\n * wrong: the bidi rule matches only U+202A-202E and U+2066-2069, and the control-character\n * rule only C0/C1 — neither matches U+200B. A zero-width space inside a SQL keyword\n * therefore raised **no finding at all**, and `1 U<ZWSP>NION SELECT` scored zero.\n *\n * The severity stays `medium`/`medium` rather than rising, because at the human-text\n * sinks the same code points are orthographic in Persian and Hindi and structural in\n * every ZWJ emoji sequence. The finding is raised everywhere; the verdict is left to\n * scoring.\n */\nconst INVISIBLE_CONTEXTS = [\n\t\"general_text\",\n\t\"html\",\n\t\"identifier\",\n\t\"object_merge\",\n\t\"url\",\n\t\"url_parameter\",\n\t\"sql\",\n\t\"nosql\",\n\t\"shell\",\n\t\"filesystem\",\n\t\"ldap\",\n\t\"xpath\",\n\t\"xml\",\n\t\"template\",\n\t\"spreadsheet\",\n\t\"jwt\",\n\t\"http_header\",\n\t\"log\",\n\t\"llm_prompt\",\n] as const;\n\n/**\n * Rules that apply in every context, including `general_text`.\n *\n * The bar for membership is high: a rule belongs here only if there is no legitimate\n * reason for the character to appear in *any* user-supplied value. Bidirectional\n * overrides (the \"Trojan Source\" class, CVE-2021-42574), zero-width characters used\n * to spoof identity, and C0 control characters all clear it. SQL keywords and `../`\n * emphatically do not, which is why they are context-scoped instead.\n */\nexport const UNIVERSAL_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"UNICODE-BIDI-OVERRIDE-001\",\n\t\ttype: \"homoglyph\",\n\t\tcontexts: ALL_THREAT_CONTEXTS,\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription:\n\t\t\t\"Bidirectional override character — reorders rendered text away from its logical order\",\n\t\tcwe: 451,\n\t\tprimaryControl: \"Reject bidi controls outright, or render with them stripped\",\n\t\tpattern: /[\\u202a-\\u202e\\u2066-\\u2069]/,\n\t},\n\t{\n\t\tid: \"UNICODE-INVISIBLE-001\",\n\t\ttype: \"homoglyph\",\n\t\tcontexts: INVISIBLE_CONTEXTS,\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Zero-width or invisible formatting character\",\n\t\tcwe: 1007,\n\t\tprimaryControl: \"Strip default-ignorable code points before comparison or storage\",\n\t\tpattern: /[\\u00ad\\u200b-\\u200f\\u2060-\\u2064\\ufeff]/,\n\t},\n\t{\n\t\tid: \"UNICODE-INVISIBLE-IDENTIFIER-001\",\n\t\ttype: \"homoglyph\",\n\t\t// Scoped to identifiers alone, and graded harder than UNICODE-INVISIBLE-001.\n\t\t// ZWNJ and ZWJ are required for correct Persian and Hindi rendering and appear\n\t\t// in ordinary emoji sequences, so the general rule stays at medium/medium. In a\n\t\t// username, domain, or package name there is no such defence: an invisible\n\t\t// character exists only to make two different identifiers render alike.\n\t\tcontexts: [\"identifier\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Invisible formatting character in a protected identifier\",\n\t\tcwe: 1007,\n\t\tprimaryControl:\n\t\t\t\"Strip default-ignorable code points, then compare UTS #39 skeletons at registration time\",\n\t\tpattern: /[\\u00ad\\u200b-\\u200f\\u2060-\\u2064\\ufeff]/,\n\t},\n\t{\n\t\tid: \"CONTROL-CHAR-001\",\n\t\ttype: \"resource_abuse\",\n\t\tcontexts: ALL_THREAT_CONTEXTS,\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"C0/C1 control character other than tab, CR, or LF\",\n\t\tcwe: 74,\n\t\tprimaryControl: \"Reject or strip control characters at the trust boundary\",\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: detecting control characters is this rule's entire purpose\n\t\tpattern: /[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/,\n\t},\n];\n\n//#endregion\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAM,cACL;;AAGD,MAAa,sBAA6C;CACzD;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,KAAK;EAChB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,KAAK;EAChB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,KAAK;EAChB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,KAAK;EAChB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,KAAK;EAChB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;AACD;;AAOA,MAAM,eACL;;AAGD,MAAa,2BAAkD;CAC9D;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,UAAU;EACrB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,UAAU;EACrB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,UAAU;EACrB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAMhB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,UAAU;EACrB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,UAAU;EACrB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAEhB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY,MAAM;EAC7B,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBACC;EACD,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EAKN,UAAU,CAAC,UAAU;EACrB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,UAAU;EACrB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,UAAU;EACrB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,UAAU;EACrB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAMhB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,UAAU;EACrB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,UAAU;EACrB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;AACD;;AAOA,MAAM,iBACL;;;;;;;;;AAUD,MAAa,yBAAgD;CAC5D;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;AACD;;;;;;;;;;AAqDA,MAAa,kBAAyC;CACrD;EACC,IAAI;EACJ,MAAM;EACN,UAAU;EACV,UAAU;EACV,YAAY;EACZ,aACC;EACD,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU;GA9CX;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EA4BW;EACV,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EAMN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBACC;EACD,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU;EACV,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAEhB,SAAS;CACV;AACD"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ThreatRule } from "../types.mjs";
|
|
2
|
+
//#region src/threats/rules/protocol.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Parameter pollution.
|
|
5
|
+
*
|
|
6
|
+
* Only the *injected-parameter* half is detectable here. Duplicate-key pollution —
|
|
7
|
+
* `?id=1&id=2` exploiting parser disagreement between ASP.NET, PHP, and Servlet
|
|
8
|
+
* stacks — is not: the engine sees one string and never the parameter map, and
|
|
9
|
+
* duplicate keys are legal and routine (`?tag=a&tag=b`). That half is a parsing
|
|
10
|
+
* decision, documented as such rather than faked with a signature.
|
|
11
|
+
*/
|
|
12
|
+
declare const PARAMETER_POLLUTION_RULES: readonly ThreatRule[];
|
|
13
|
+
/**
|
|
14
|
+
* Credential material appearing where it will be stored, logged, or sent onward.
|
|
15
|
+
*
|
|
16
|
+
* A generic `password=…` / `api_key: …` assignment rule was written and then dropped:
|
|
17
|
+
* it produced false positives on every realistic log line of the form
|
|
18
|
+
* `"api_key: required for this endpoint"` and `"validation failed: password: too_short"`.
|
|
19
|
+
* Shipping it weakened would trade the category's credibility for coverage.
|
|
20
|
+
*/
|
|
21
|
+
declare const CREDENTIAL_EXPOSURE_RULES: readonly ThreatRule[];
|
|
22
|
+
/**
|
|
23
|
+
* Double percent-encoding.
|
|
24
|
+
*
|
|
25
|
+
* `%253Cscript%253E` decodes once to `%3Cscript%3E` and twice to `<script>`. The
|
|
26
|
+
* scanner decodes once, so the payload was invisible to every signature — verified at
|
|
27
|
+
* 0/allow before this rule existed. The traversal case had an explicit
|
|
28
|
+
* `%252e%252e%252f` rule; nothing covered the general class.
|
|
29
|
+
*
|
|
30
|
+
* Its own category rather than a subtype of whatever it eventually decodes to: at scan
|
|
31
|
+
* time the eventual sink is unknown, and the signal — "this input is shaped to survive
|
|
32
|
+
* one decoding pass" — is itself the finding. The real fix is to decode exactly once,
|
|
33
|
+
* at a defined boundary, and never again.
|
|
34
|
+
*/
|
|
35
|
+
declare const DOUBLE_ENCODING_RULES: readonly ThreatRule[];
|
|
36
|
+
/**
|
|
37
|
+
* JWT tampering.
|
|
38
|
+
*
|
|
39
|
+
* Scoped to the unsecured-token case alone. `kid`, `jku`, and `x5u` injection are
|
|
40
|
+
* *already covered* — extract the claim and declare its real sink, and the existing
|
|
41
|
+
* rules fire: `kid=../../../../dev/null` with `filesystem` hits PATH-TRAVERSAL-001,
|
|
42
|
+
* `jku=http://169.254.169.254/` with `url` hits SSRF-METADATA-001. Re-scanning a whole
|
|
43
|
+
* opaque token with every detector would be the run-everything-against-everything
|
|
44
|
+
* anti-pattern the engine exists to avoid.
|
|
45
|
+
*/
|
|
46
|
+
declare const JWT_RULES: readonly ThreatRule[];
|
|
47
|
+
//#endregion
|
|
48
|
+
export { CREDENTIAL_EXPOSURE_RULES, DOUBLE_ENCODING_RULES, JWT_RULES, PARAMETER_POLLUTION_RULES };
|
|
49
|
+
//# sourceMappingURL=protocol.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.mts","names":[],"sources":["../../../src/threats/rules/protocol.ts"],"mappings":";;;;;;;;;;;cA0Ca,oCAAoC;;;;;;;;;cA2DpC,oCAAoC;;;;;;;;;;;;;;cAwEpC,gCAAgC;;;;;;;;;;;cAyDhC,oBAAoB"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
//#region src/threats/rules/protocol.ts
|
|
2
|
+
/**
|
|
3
|
+
* Parameter pollution.
|
|
4
|
+
*
|
|
5
|
+
* Only the *injected-parameter* half is detectable here. Duplicate-key pollution —
|
|
6
|
+
* `?id=1&id=2` exploiting parser disagreement between ASP.NET, PHP, and Servlet
|
|
7
|
+
* stacks — is not: the engine sees one string and never the parameter map, and
|
|
8
|
+
* duplicate keys are legal and routine (`?tag=a&tag=b`). That half is a parsing
|
|
9
|
+
* decision, documented as such rather than faked with a signature.
|
|
10
|
+
*/
|
|
11
|
+
const PARAMETER_POLLUTION_RULES = [{
|
|
12
|
+
id: "PARAM-POLLUTION-PRIVILEGED-001",
|
|
13
|
+
type: "parameter_pollution",
|
|
14
|
+
contexts: ["url_parameter"],
|
|
15
|
+
severity: "high",
|
|
16
|
+
confidence: "medium",
|
|
17
|
+
description: "Injected parameter naming a privilege, price, or credential field",
|
|
18
|
+
cwe: 235,
|
|
19
|
+
primaryControl: "Build query strings with URLSearchParams or encodeURIComponent per value; read parameters through one parser and reject duplicate keys server-side",
|
|
20
|
+
pattern: /[&;][a-z0-9_.[-]{0,24}(?:role|is_?admin|admin|user_?id|userid|uid|account_?id|amount|price|total|quantity|qty|access_?token|api_?key|apikey|client_?secret|secret|password|passwd|pwd|session_?id|sessionid|signature|hmac|permissions?|privilege|is_?staff|superuser|debug)[\]"']{0,2}\s{0,4}=/i
|
|
21
|
+
}, {
|
|
22
|
+
id: "PARAM-POLLUTION-APPEND-001",
|
|
23
|
+
type: "parameter_pollution",
|
|
24
|
+
contexts: ["url_parameter"],
|
|
25
|
+
severity: "medium",
|
|
26
|
+
confidence: "low",
|
|
27
|
+
description: "Value appends an additional query parameter",
|
|
28
|
+
cwe: 235,
|
|
29
|
+
primaryControl: "Build query strings with URLSearchParams or encodeURIComponent per value; read parameters through one parser and reject duplicate keys server-side",
|
|
30
|
+
pattern: /^[^?]{0,256}&[a-z_][a-z0-9_.-]{1,48}(?:\[[a-z0-9_.-]{0,32}\])?\s{0,4}=/i
|
|
31
|
+
}];
|
|
32
|
+
/** Repeated across the URL-borne credential rules. */
|
|
33
|
+
const CRED_URL_CONTROL = "Carry credentials in the Authorization header or a POST body, never a query string; set Referrer-Policy: no-referrer and rotate any token that reached a URL";
|
|
34
|
+
/**
|
|
35
|
+
* Credential material appearing where it will be stored, logged, or sent onward.
|
|
36
|
+
*
|
|
37
|
+
* A generic `password=…` / `api_key: …` assignment rule was written and then dropped:
|
|
38
|
+
* it produced false positives on every realistic log line of the form
|
|
39
|
+
* `"api_key: required for this endpoint"` and `"validation failed: password: too_short"`.
|
|
40
|
+
* Shipping it weakened would trade the category's credibility for coverage.
|
|
41
|
+
*/
|
|
42
|
+
const CREDENTIAL_EXPOSURE_RULES = [
|
|
43
|
+
{
|
|
44
|
+
id: "CRED-URL-QUERY-001",
|
|
45
|
+
type: "credential_exposure",
|
|
46
|
+
contexts: ["url", "log"],
|
|
47
|
+
severity: "high",
|
|
48
|
+
confidence: "medium",
|
|
49
|
+
description: "Credential-bearing query parameter — leaks via Referer, proxies, and logs",
|
|
50
|
+
cwe: 598,
|
|
51
|
+
primaryControl: CRED_URL_CONTROL,
|
|
52
|
+
pattern: /[?&](?:access[_-]?token|auth[_-]?token|id[_-]?token|refresh[_-]?token|api[_-]?key|api[_-]?secret|client[_-]?secret|secret[_-]?key|session[_-]?id|session[_-]?token|jsessionid|phpsessid|password|passwd|pwd)=[^&#\s]{4,256}/i
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "CRED-AUTH-SCHEME-001",
|
|
56
|
+
type: "credential_exposure",
|
|
57
|
+
contexts: ["log"],
|
|
58
|
+
severity: "high",
|
|
59
|
+
confidence: "high",
|
|
60
|
+
description: "Authorization header value written to a log",
|
|
61
|
+
cwe: 532,
|
|
62
|
+
primaryControl: "Log structured records with an allowlist of loggable fields; redact by key with safeStringify",
|
|
63
|
+
pattern: /\b(?:proxy-)?authorization\s{0,4}:\s{0,4}(?:bearer|basic|digest|token|apikey)\s{0,4}[A-Za-z0-9+/=._~-]{8,512}/i
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "CRED-JWT-001",
|
|
67
|
+
type: "credential_exposure",
|
|
68
|
+
contexts: ["url", "log"],
|
|
69
|
+
severity: "high",
|
|
70
|
+
confidence: "high",
|
|
71
|
+
description: "JSON Web Token in a URL or log line",
|
|
72
|
+
cwe: 522,
|
|
73
|
+
primaryControl: CRED_URL_CONTROL,
|
|
74
|
+
pattern: /\beyJ[A-Za-z0-9_-]{10,400}\.eyJ[A-Za-z0-9_-]{10,400}\./
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "CRED-PROVIDER-KEY-001",
|
|
78
|
+
type: "credential_exposure",
|
|
79
|
+
contexts: ["url", "log"],
|
|
80
|
+
severity: "critical",
|
|
81
|
+
confidence: "high",
|
|
82
|
+
description: "Provider API key or personal access token",
|
|
83
|
+
cwe: 532,
|
|
84
|
+
primaryControl: "Load provider keys from a secret manager, never from source or a URL; rotate immediately on exposure",
|
|
85
|
+
pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b|\bgh[pousr]_[A-Za-z0-9]{36}\b|\bgithub_pat_[A-Za-z0-9_]{22,82}\b|\bxox[abprs]-[A-Za-z0-9-]{10,120}|\bsk_live_[A-Za-z0-9]{16,64}\b|\bsk-(?:ant|proj)-[A-Za-z0-9_-]{20,120}/
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
/**
|
|
89
|
+
* Double percent-encoding.
|
|
90
|
+
*
|
|
91
|
+
* `%253Cscript%253E` decodes once to `%3Cscript%3E` and twice to `<script>`. The
|
|
92
|
+
* scanner decodes once, so the payload was invisible to every signature — verified at
|
|
93
|
+
* 0/allow before this rule existed. The traversal case had an explicit
|
|
94
|
+
* `%252e%252e%252f` rule; nothing covered the general class.
|
|
95
|
+
*
|
|
96
|
+
* Its own category rather than a subtype of whatever it eventually decodes to: at scan
|
|
97
|
+
* time the eventual sink is unknown, and the signal — "this input is shaped to survive
|
|
98
|
+
* one decoding pass" — is itself the finding. The real fix is to decode exactly once,
|
|
99
|
+
* at a defined boundary, and never again.
|
|
100
|
+
*/
|
|
101
|
+
const DOUBLE_ENCODING_RULES = [{
|
|
102
|
+
id: "ENCODING-DOUBLE-PERCENT-002",
|
|
103
|
+
type: "double_encoding",
|
|
104
|
+
contexts: [
|
|
105
|
+
"url",
|
|
106
|
+
"url_parameter",
|
|
107
|
+
"html",
|
|
108
|
+
"filesystem",
|
|
109
|
+
"sql",
|
|
110
|
+
"http_header",
|
|
111
|
+
"xml"
|
|
112
|
+
],
|
|
113
|
+
severity: "high",
|
|
114
|
+
confidence: "high",
|
|
115
|
+
description: "Percent sign and its hex digits each encoded separately",
|
|
116
|
+
cwe: 177,
|
|
117
|
+
primaryControl: "Decode exactly once at a defined boundary and never re-decode; reject input that still contains an escape prefix after decoding",
|
|
118
|
+
pattern: /%25%[0-9a-f]{2}%[0-9a-f]{2}/i
|
|
119
|
+
}, {
|
|
120
|
+
id: "ENCODING-DOUBLE-PERCENT-001",
|
|
121
|
+
type: "double_encoding",
|
|
122
|
+
contexts: [
|
|
123
|
+
"url",
|
|
124
|
+
"url_parameter",
|
|
125
|
+
"html",
|
|
126
|
+
"filesystem",
|
|
127
|
+
"sql",
|
|
128
|
+
"http_header",
|
|
129
|
+
"xml"
|
|
130
|
+
],
|
|
131
|
+
severity: "high",
|
|
132
|
+
confidence: "medium",
|
|
133
|
+
description: "Escape prefix that decodes to a metacharacter only after a second pass",
|
|
134
|
+
cwe: 177,
|
|
135
|
+
primaryControl: "Decode exactly once at a defined boundary and never re-decode; reject input that still contains an escape prefix after decoding",
|
|
136
|
+
variants: ["raw"],
|
|
137
|
+
pattern: /%25(?:2[0257CEFcef]|3[CEce]|22|26|27|28|29|3[BbDd]|5[Cc]|00|0[ADad])/
|
|
138
|
+
}];
|
|
139
|
+
/** Repeated across the JWT rules. */
|
|
140
|
+
const JWT_CONTROL = "Verify with an explicit algorithm allowlist (jwtVerify(token, key, { algorithms: ['RS256'] })); never let the token's own header select the algorithm, and reject 'none' unconditionally";
|
|
141
|
+
/**
|
|
142
|
+
* JWT tampering.
|
|
143
|
+
*
|
|
144
|
+
* Scoped to the unsecured-token case alone. `kid`, `jku`, and `x5u` injection are
|
|
145
|
+
* *already covered* — extract the claim and declare its real sink, and the existing
|
|
146
|
+
* rules fire: `kid=../../../../dev/null` with `filesystem` hits PATH-TRAVERSAL-001,
|
|
147
|
+
* `jku=http://169.254.169.254/` with `url` hits SSRF-METADATA-001. Re-scanning a whole
|
|
148
|
+
* opaque token with every detector would be the run-everything-against-everything
|
|
149
|
+
* anti-pattern the engine exists to avoid.
|
|
150
|
+
*/
|
|
151
|
+
const JWT_RULES = [{
|
|
152
|
+
id: "JWT-ALG-NONE-UNSECURED-001",
|
|
153
|
+
type: "jwt_tampering",
|
|
154
|
+
contexts: ["jwt", "http_header"],
|
|
155
|
+
severity: "high",
|
|
156
|
+
confidence: "high",
|
|
157
|
+
description: "Unsecured JWT — three segments with an empty signature",
|
|
158
|
+
cwe: 347,
|
|
159
|
+
primaryControl: JWT_CONTROL,
|
|
160
|
+
pattern: /(?:^|[\s,;=("'[])eyJ[A-Za-z0-9_-]{16,2000}\.eyJ[A-Za-z0-9_-]{8,4000}\.(?:$|[\s,;)"'\]&#])/
|
|
161
|
+
}, {
|
|
162
|
+
id: "JWT-ALG-NONE-HEADER-001",
|
|
163
|
+
type: "jwt_tampering",
|
|
164
|
+
contexts: ["jwt"],
|
|
165
|
+
severity: "high",
|
|
166
|
+
confidence: "high",
|
|
167
|
+
description: "Decoded JWT header selecting the 'none' algorithm",
|
|
168
|
+
cwe: 347,
|
|
169
|
+
primaryControl: JWT_CONTROL,
|
|
170
|
+
pattern: /"alg"\s{0,8}:\s{0,8}"\s{0,8}none\s{0,8}"/i
|
|
171
|
+
}];
|
|
172
|
+
//#endregion
|
|
173
|
+
export { CREDENTIAL_EXPOSURE_RULES, DOUBLE_ENCODING_RULES, JWT_RULES, PARAMETER_POLLUTION_RULES };
|
|
174
|
+
|
|
175
|
+
//# sourceMappingURL=protocol.mjs.map
|