lambda-live-debugger
Version:
Debug Lambda functions locally like it is running in the cloud
266 lines (231 loc) • 8.73 kB
JavaScript
/**
* is-unsafe v2
*
* Zero-dependency, DOM-free, pure predicate for detecting unsafe strings
* across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts.
*
* v2 change: contexts are imported as named pattern arrays rather than resolved
* via a string-keyed registry. This makes each context independently
* tree-shakeable — bundlers can drop any context you never import.
*
* @module is-unsafe
*/
// ─── Context pattern arrays (named exports) ────────────────────────────────
// Import only the ones you need. Each is independently tree-shakeable.
export { default as HTML } from './contexts/html.js';
export { default as XML } from './contexts/xml.js';
export { default as SVG } from './contexts/svg.js';
export { default as SQL } from './contexts/sql.js';
export { default as SHELL } from './contexts/shell.js';
export { default as REDOS } from './contexts/redos.js';
export { default as NOSQL } from './contexts/nosql.js';
export { default as LOG } from './contexts/log.js';
// SQL-STRICT needs a quoted identifier because of the hyphen
import SQL_STRICT from './contexts/sql-strict.js';
export { SQL_STRICT };
// ─── VALID_CONTEXTS convenience re-export ─────────────────────────────────
// Importing this pulls in ALL contexts. Use it only when you need all of them
// (e.g. for validation UI, tooling, or exhaustive audits).
// If you only need a subset, import the named contexts directly instead.
import HTML from './contexts/html.js';
import XML from './contexts/xml.js';
import SVG from './contexts/svg.js';
import SQL from './contexts/sql.js';
import SHELL from './contexts/shell.js';
import REDOS from './contexts/redos.js';
import NOSQL from './contexts/nosql.js';
import LOG from './contexts/log.js';
// ─── Attach labels to named contexts ──────────────────────────────────────
// Each built-in PatternList carries its canonical name so matchList can read
// list.label directly — no registry lookup needed at match time.
// Custom PatternLists default to 'CUSTOM' unless the caller sets list.label.
HTML.label = 'HTML';
XML.label = 'XML';
SVG.label = 'SVG';
SQL.label = 'SQL';
SQL_STRICT.label = 'SQL-STRICT';
SHELL.label = 'SHELL';
REDOS.label = 'REDOS';
NOSQL.label = 'NOSQL';
LOG.label = 'LOG';
export const VALID_CONTEXTS = Object.freeze({
HTML,
XML,
SVG,
SQL,
'SQL-STRICT': SQL_STRICT,
SHELL,
REDOS,
NOSQL,
LOG,
});
// ─── Types ────────────────────────────────────────────────────────────────
/**
* @typedef {{ id: string, description: string, pattern: RegExp }} Rule
*/
/**
* @typedef {Rule[]} PatternList
*/
/**
* @typedef {Object} MatchResult
* @property {string} context - Label identifying which context matched ('HTML', 'CUSTOM', etc.)
* @property {string} id - Rule identifier
* @property {string} description - Human-readable description of what was matched
* @property {RegExp} pattern - The pattern that matched
*/
// ─── Internal helpers ──────────────────────────────────────────────────────
/**
* @param {unknown} value
*/
function assertString(value) {
if (typeof value !== 'string') {
throw new TypeError(
`is-unsafe: first argument must be a string, got ${typeof value}`
);
}
}
/**
* @param {unknown} context
*/
function assertContext(context) {
if (context instanceof RegExp) return;
if (Array.isArray(context)) {
if (context.length === 0) {
throw new TypeError('is-unsafe: context must not be an empty array');
}
// Detect array-of-arrays vs flat pattern list
if (Array.isArray(context[0])) {
// Array of PatternLists
for (const list of context) {
if (!Array.isArray(list) || list.length === 0) {
throw new TypeError(
'is-unsafe: each context in the array must be a non-empty pattern array (PatternList)'
);
}
}
}
// else: flat PatternList — trust it, no deep validation needed
return;
}
throw new TypeError(
`is-unsafe: second argument must be a PatternList (e.g. HTML), ` +
`an array of PatternLists (e.g. [HTML, XML]), or a RegExp. Got: ${typeof context}`
);
}
/**
* Normalise any valid context arg into an array of PatternLists.
*
* @param {Rule[]|Rule[][]|RegExp} context
* @returns {{ lists: Rule[][]|null, regex: RegExp|null }}
*/
function normalise(context) {
if (context instanceof RegExp) return { lists: null, regex: context };
// Distinguish PatternList (array of rule objects) from array of PatternLists
if (Array.isArray(context[0])) return { lists: context, regex: null };
return { lists: [context], regex: null };
}
/**
* Test value against a single PatternList. Returns the first MatchResult or null.
*
* @param {string} value
* @param {Rule[]} list
* @returns {MatchResult|null}
*/
function matchList(value, list) {
const label = list.label ?? 'CUSTOM';
for (const rule of list) {
if (rule.pattern.test(value)) {
return { context: label, 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 {PatternList | PatternList[] | RegExp} context
* - A PatternList imported from is-unsafe (e.g. `HTML`, `XML`)
* - An array of PatternLists — returns true if unsafe in **any** of them
* - A custom RegExp — returns true if the pattern matches
* @returns {boolean}
*
* @example
* import { isUnsafe, HTML, SQL } from 'is-unsafe';
*
* 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);
const { lists, regex } = normalise(context);
if (regex) return regex.test(value);
for (const list of lists) {
if (matchList(value, list) !== null) return true;
}
return false;
}
/**
* Like `isUnsafe`, but returns the first `MatchResult` describing **why**
* the value was flagged, or `null` if it is safe.
*
* @param {string} value
* @param {PatternList | PatternList[] | RegExp} context
* @returns {MatchResult|null}
*
* @example
* import { whyUnsafe, HTML } from 'is-unsafe';
*
* whyUnsafe('<script>alert(1)</script>', HTML)
* // { context: 'HTML', id: 'html-script-open', description: '...', pattern: /.../ }
*/
function whyUnsafe(value, context) {
assertString(value);
assertContext(context);
const { lists, regex } = normalise(context);
if (regex) {
return regex.test(value)
? { context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex }
: null;
}
for (const list of lists) {
const result = matchList(value, list);
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 {PatternList | PatternList[] | RegExp} context
* @returns {MatchResult[]}
*/
function allUnsafe(value, context) {
assertString(value);
assertContext(context);
const { lists, regex } = normalise(context);
const results = [];
if (regex) {
if (regex.test(value)) {
results.push({ context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex });
}
return results;
}
for (const list of lists) {
const label = list.label ?? 'CUSTOM';
for (const rule of list) {
if (rule.pattern.test(value)) {
results.push({ context: label, id: rule.id, description: rule.description, pattern: rule.pattern });
}
}
}
return results;
}
export { isUnsafe, whyUnsafe, allUnsafe };
export default isUnsafe;