purifai 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -31,175 +31,241 @@ __export(index_exports, {
31
31
  sanitizeBatch: () => sanitizeBatch
32
32
  });
33
33
  module.exports = __toCommonJS(index_exports);
34
- var DANGEROUS_TAGS_WITH_CONTENT = /<(script|style|iframe|frame|object|embed|applet|meta|link|form|svg|math|base)(?:\s[^>]*)?>[\s\S]*?<\/\1>|<(script|style|iframe|frame|object|embed|applet|meta|link|form|svg|math|base)(?:\s[^>]*)?\/?>|<\/(script|style|iframe|frame|object|embed|applet|meta|link|form|svg|math|base)>/gi;
35
- var EVENT_HANDLERS_ENHANCED = /\s*o\s*n\s*[a-z]+\s*=\s*["']?[^"'>]+["']?/gi;
36
- var DANGEROUS_PROTOCOLS_ENHANCED = /(?:href|src|action|formaction|data|background|poster|code|cite|longdesc|usemap|itemtype|ping|manifest|archive|classid|codebase|datasrc|dynsrc|lowsrc|srcset)\s*=\s*["']?\s*(?:j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*:|javascript\s*:|vbscript\s*:|data:text\/html|data:image\/svg\+xml(?:[^"'>]*script)?|filesystem:|chrome-extension:|blob:|about:|res:|ie:|ms-its:|mk:|mhtml:|file:|jar:|hcp:|ms-help:|disk:|vnd\.ms-|shell:|lynxcgi:|lynxexec:|news:|nntp:|telnet:|gopher:|wais:|prospero:|webcal:|ldap:|ldaps:|ftp:|ftps:|sftp:|ssh:|ircs?:|mailto:|xmpp:|sms:|smsto:|mms:|mmsto:|tel:|fax:|modem:|payto:|bitcoin:|ethereum:|magnet:)[^"'\s>]*/gi;
37
- var DANGEROUS_ATTRIBUTES = /(?:expression|@import|javascript:|vbscript:|livescript:|mocha:|behavior:|constructor)\s*\(/gi;
38
- var TEMPLATE_INJECTION = /\{\{[\s\S]*?\}\}|<%[\s\S]*?%>|<\?[\s\S]*?\?>|\${[\s\S]*?}|#\{[\s\S]*?}/g;
39
- var DANGEROUS_FUNCTIONS = /\b(?:alert|eval|expression|Function|constructor|prototype|__proto__|document\.write|document\.writeln|window\.location|document\.location|setTimeout|setInterval|setImmediate|execScript|msSetImmediate|range\.createContextualFragment|range\.insertNode|insertAdjacentHTML|outerHTML)\s*\(/gi;
40
34
  var NULL_CONTROL_CHARS = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g;
41
- var POLYGLOT_JAVASCRIPT = /j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*:/gi;
42
- var POLYGLOT_EVENTS = /o\s*n\s*[a-z]+\s*(?:=|alert)/gi;
43
- var ENCODED_TAGS = /\\x3c|\\x3e|%3c|%3e/gi;
44
- var SUSPICIOUS_PATTERNS = /<|>|javascript:|vbscript:|\bon[a-z]+\s*=|@import|\{\{|<%|<\?|\${|#\{/i;
45
- var DANGEROUS_PROTOCOL_TOKEN = /(?:j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t|v\s*b\s*s\s*c\s*r\s*i\s*p\s*t|l\s*i\s*v\s*e\s*s\s*c\s*r\s*i\s*p\s*t|m\s*o\s*c\s*h\s*a)\s*:|data\s*:\s*text\/html/i;
46
- var HAS_TEXT_CONTENT = /[a-z0-9]/i;
47
- var HTML_TAG = /<\/?[a-zA-Z][^>]*>/g;
35
+ var HAS_CONTROL_CHARS = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/;
36
+ var ENCODED_SYNTAX = /%3[ce]|\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|&#(?:\d+|x[0-9a-f]+);/i;
37
+ var DANGEROUS_CONTAINER_PATTERN = /<\s*\/?\s*(?:script|style|iframe|object|applet|svg|math|form|template|noscript|noframes|xmp|plaintext|head|title|textarea|frameset)\b/i;
38
+ var EVENT_HANDLER_PATTERN = /\bo\s*n\s*(?:abort|afterprint|animationend|animationiteration|animationstart|beforeinput|beforeprint|beforeunload|begin|blur|canplay|change|click|close|contextmenu|copy|cut|dblclick|drag|drop|durationchange|ended|error|focus|focusin|focusout|hashchange|input|invalid|keydown|keypress|keyup|load|loadeddata|loadedmetadata|loadstart|message|mousedown|mouseenter|mouseleave|mousemove|mouseout|mouseover|mouseup|offline|online|open|pagehide|pageshow|paste|pause|play|playing|popstate|progress|ratechange|reset|resize|scroll|search|seeked|seeking|select|show|stalled|start|storage|submit|suspend|timeupdate|toggle|touchcancel|touchend|touchmove|touchstart|transitionend|unload|volumechange|waiting|wheel)\s*=/i;
39
+ var DANGEROUS_PROTOCOL_PATTERN = /(?:j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t|v\s*b\s*s\s*c\s*r\s*i\s*p\s*t|l\s*i\s*v\s*e\s*s\s*c\s*r\s*i\s*p\s*t|m\s*o\s*c\s*h\s*a)\s*:|data\s*:\s*(?:text\/html|image\/svg\+xml)/i;
40
+ var DANGEROUS_CODE_PATTERN = /(?:\b(?:eval|expression|Function|constructor|document\.write|insertAdjacentHTML)\s*\(|@import\b|\{\{|<%|<\?|\${|#\{)/i;
41
+ var DROP_BODY_TAGS = /* @__PURE__ */ new Set([
42
+ "applet",
43
+ "form",
44
+ "frameset",
45
+ "head",
46
+ "iframe",
47
+ "math",
48
+ "noframes",
49
+ "noscript",
50
+ "object",
51
+ "plaintext",
52
+ "script",
53
+ "style",
54
+ "svg",
55
+ "template",
56
+ "textarea",
57
+ "title",
58
+ "xmp"
59
+ ]);
60
+ var SAFE_URL_PROTOCOLS = /* @__PURE__ */ new Set(["http", "https", "mailto"]);
48
61
  function escapeAngleBrackets(str) {
49
62
  return str.replace(/</g, "&lt;").replace(/>/g, "&gt;");
50
63
  }
51
- function stateless(pattern) {
52
- return new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, ""));
53
- }
54
- var DETECT_DANGEROUS_TAGS = stateless(DANGEROUS_TAGS_WITH_CONTENT);
55
- var DETECT_EVENT_HANDLERS = stateless(EVENT_HANDLERS_ENHANCED);
56
- var DETECT_DANGEROUS_PROTOCOLS = stateless(DANGEROUS_PROTOCOLS_ENHANCED);
57
- var DETECT_DANGEROUS_ATTRIBUTES = stateless(DANGEROUS_ATTRIBUTES);
58
- var DETECT_TEMPLATE_INJECTION = stateless(TEMPLATE_INJECTION);
59
- var DETECT_POLYGLOT_JAVASCRIPT = stateless(POLYGLOT_JAVASCRIPT);
60
- var DETECT_POLYGLOT_EVENTS = stateless(POLYGLOT_EVENTS);
61
64
  var DEFAULT_OPTIONS = {
62
65
  maxLength: 1e6,
63
66
  // 1MB
64
67
  allowedProtocols: ["http", "https", "mailto"],
65
68
  aggressiveMode: true
66
69
  };
67
- var VERSION = "2.0.2";
68
- var MAX_FILTER_PASSES = 5;
70
+ var VERSION = "2.0.3";
71
+ function isDisallowedScalar(codePoint) {
72
+ return !Number.isInteger(codePoint) || codePoint <= 0 || codePoint > 1114111 || codePoint >= 55296 && codePoint <= 57343 || codePoint < 32 && codePoint !== 9 && codePoint !== 10 && codePoint !== 13 || codePoint >= 127 && codePoint <= 159;
73
+ }
74
+ function scalarFromDigits(digits, radix) {
75
+ const codePoint = Number.parseInt(digits, radix);
76
+ return isDisallowedScalar(codePoint) ? "\uFFFD" : String.fromCodePoint(codePoint);
77
+ }
69
78
  function decodeEncodingBypasses(str) {
70
- let result = str;
71
- try {
72
- result = decodeURIComponent(result);
73
- } catch {
79
+ return str.replace(/%3c/gi, "<").replace(/%3e/gi, ">").replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => scalarFromDigits(hex, 16)).replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => scalarFromDigits(hex, 16)).replace(/&#(\d+);/g, (_, digits) => scalarFromDigits(digits, 10)).replace(/&#x([0-9a-fA-F]+);/gi, (_, digits) => scalarFromDigits(digits, 16));
80
+ }
81
+ function isAsciiLetter(char) {
82
+ if (!char) return false;
83
+ const code = char.charCodeAt(0);
84
+ return code >= 65 && code <= 90 || code >= 97 && code <= 122;
85
+ }
86
+ function isAsciiWhitespace(char) {
87
+ return char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
88
+ }
89
+ function isTagNameChar(char) {
90
+ if (!char) return false;
91
+ const code = char.charCodeAt(0);
92
+ return isAsciiLetter(char) || code >= 48 && code <= 57 || char === ":" || char === "-";
93
+ }
94
+ var INCOMPLETE_TAG = /* @__PURE__ */ Symbol("incomplete-tag");
95
+ function parseTag(input, start) {
96
+ if (input[start] !== "<") return null;
97
+ let cursor = start + 1;
98
+ let closing = false;
99
+ if (input[cursor] === "/") {
100
+ closing = true;
101
+ cursor++;
102
+ while (isAsciiWhitespace(input[cursor])) cursor++;
74
103
  }
75
- try {
76
- result = result.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => {
77
- return String.fromCharCode(parseInt(hex, 16));
78
- });
79
- result = result.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => {
80
- return String.fromCharCode(parseInt(hex, 16));
81
- });
82
- result = result.replace(/&#(\d+);/g, (_, num) => {
83
- return String.fromCharCode(parseInt(num, 10));
84
- });
85
- result = result.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => {
86
- return String.fromCharCode(parseInt(hex, 16));
87
- });
88
- } catch {
104
+ if (!isAsciiLetter(input[cursor])) return null;
105
+ const nameStart = cursor;
106
+ while (isTagNameChar(input[cursor])) cursor++;
107
+ const name = input.slice(nameStart, cursor).toLowerCase();
108
+ const boundary = input[cursor];
109
+ if (boundary === void 0) return INCOMPLETE_TAG;
110
+ if (boundary !== ">" && boundary !== "/" && !isAsciiWhitespace(boundary)) return null;
111
+ if (isAsciiWhitespace(boundary)) {
112
+ let next = cursor;
113
+ while (isAsciiWhitespace(input[next])) next++;
114
+ if (input[next] === "&") return null;
115
+ }
116
+ let quote = "";
117
+ let selfClosing = false;
118
+ for (; cursor < input.length; cursor++) {
119
+ const char = input[cursor];
120
+ if (quote) {
121
+ if (char === quote) quote = "";
122
+ continue;
123
+ }
124
+ if (char === '"' || char === "'") {
125
+ quote = char;
126
+ continue;
127
+ }
128
+ if (char === ">") {
129
+ let before = cursor - 1;
130
+ while (before > start && isAsciiWhitespace(input[before])) before--;
131
+ selfClosing = input[before] === "/";
132
+ return { closing, end: cursor + 1, name, selfClosing };
133
+ }
134
+ }
135
+ return INCOMPLETE_TAG;
136
+ }
137
+ function skipCommentOrDeclaration(input, start) {
138
+ if (input.startsWith("<!--", start)) {
139
+ const end = input.indexOf("-->", start + 4);
140
+ return end === -1 ? input.length : end + 3;
141
+ }
142
+ if (input.startsWith("<!", start) || input.startsWith("<?", start)) {
143
+ const end = input.indexOf(">", start + 2);
144
+ return end === -1 ? input.length : end + 1;
89
145
  }
90
- return result;
146
+ return null;
91
147
  }
92
- function removeDangerousTagsWithContent(str) {
93
- return str.replace(DANGEROUS_TAGS_WITH_CONTENT, "");
148
+ function skipContainerBody(input, cursor, containerName) {
149
+ let depth = 1;
150
+ while (cursor < input.length) {
151
+ const nextOpen = input.indexOf("<", cursor);
152
+ if (nextOpen === -1) return input.length;
153
+ const specialEnd = skipCommentOrDeclaration(input, nextOpen);
154
+ if (specialEnd !== null) {
155
+ cursor = specialEnd;
156
+ continue;
157
+ }
158
+ const tag = parseTag(input, nextOpen);
159
+ if (tag === INCOMPLETE_TAG) return input.length;
160
+ if (!tag) {
161
+ cursor = nextOpen + 1;
162
+ continue;
163
+ }
164
+ cursor = tag.end;
165
+ if (tag.name !== containerName) continue;
166
+ if (tag.closing) {
167
+ depth--;
168
+ if (depth === 0) return cursor;
169
+ } else if (!tag.selfClosing) {
170
+ depth++;
171
+ }
172
+ }
173
+ return input.length;
174
+ }
175
+ function stripToText(input) {
176
+ const output = [];
177
+ let textStart = 0;
178
+ let cursor = 0;
179
+ while (cursor < input.length) {
180
+ const nextOpen = input.indexOf("<", cursor);
181
+ if (nextOpen === -1) break;
182
+ const specialEnd = skipCommentOrDeclaration(input, nextOpen);
183
+ if (specialEnd !== null) {
184
+ output.push(input.slice(textStart, nextOpen));
185
+ cursor = specialEnd;
186
+ textStart = cursor;
187
+ continue;
188
+ }
189
+ const tag = parseTag(input, nextOpen);
190
+ if (tag === INCOMPLETE_TAG) {
191
+ output.push(input.slice(textStart));
192
+ textStart = input.length;
193
+ cursor = input.length;
194
+ break;
195
+ }
196
+ if (!tag) {
197
+ cursor = nextOpen + 1;
198
+ continue;
199
+ }
200
+ output.push(input.slice(textStart, nextOpen));
201
+ cursor = tag.end;
202
+ if (!tag.closing && !tag.selfClosing && DROP_BODY_TAGS.has(tag.name)) {
203
+ cursor = skipContainerBody(input, cursor, tag.name);
204
+ }
205
+ textStart = cursor;
206
+ }
207
+ if (textStart < input.length) output.push(input.slice(textStart));
208
+ const text = output.join("").replace(/\s+/g, " ").trim();
209
+ return text.includes("<") || text.includes(">") ? escapeAngleBrackets(text) : text;
94
210
  }
95
- function handlePolyglotAttacks(str) {
96
- let result = str;
211
+ function coerceInput(input) {
212
+ if (input === null || input === void 0) return "";
97
213
  try {
98
- result = result.replace(POLYGLOT_JAVASCRIPT, "");
99
- result = result.replace(POLYGLOT_EVENTS, "");
100
- result = result.replace(ENCODED_TAGS, "");
101
- result = result.replace(/javascript\s*:\s*\/\*[\s\S]*?\*\//gi, "");
102
- result = result.replace(/javascript\s*:\s*\/\*[^*]*\*\/[^>]*/gi, "");
103
- result = result.replace(/<form[^>]*>[\s\S]*?<\/form>/gi, "");
104
- result = result.replace(/<math[^>]*>[\s\S]*?<\/math>/gi, "");
105
- result = result.replace(/%0[AD]/gi, "");
106
- if (/svg|sVg|\x3c|\\\w{3}/i.test(result)) {
107
- result = result.replace(/svg[^>]*>/gi, "");
108
- result = result.replace(/sVg[^>]*>/gi, "");
109
- result = result.replace(/\\x\w{2}/gi, "");
110
- result = result.replace(/<\/[^>]*>/gi, "");
111
- result = result.replace(/<[^>]*>/gi, "");
214
+ if (typeof input === "string") return input;
215
+ if (typeof input === "object") {
216
+ const serialized = JSON.stringify(input);
217
+ return typeof serialized === "string" ? serialized : "";
112
218
  }
219
+ return String(input);
113
220
  } catch {
114
- result = result.replace(/<[^>]*>/g, "").replace(/[<>]/g, "");
221
+ return "";
115
222
  }
116
- return result;
223
+ }
224
+ function resolveMaxLength(value) {
225
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : DEFAULT_OPTIONS.maxLength;
117
226
  }
118
227
  function assessThreatLevel(input) {
119
228
  if (!input) return "none";
120
- if (/<script/i.test(input) || /javascript\s*:/i.test(input) || /\bon[a-z]+\s*=/i.test(input)) {
229
+ if (/<\s*\/?\s*script\b/i.test(input) || DANGEROUS_PROTOCOL_PATTERN.test(input) || EVENT_HANDLER_PATTERN.test(input)) {
121
230
  return "critical";
122
231
  }
123
- if (/<(iframe|object|embed|form|svg|math)/i.test(input) || /eval\s*\(/i.test(input)) {
232
+ if (DANGEROUS_CONTAINER_PATTERN.test(input) || /\beval\s*\(/i.test(input)) {
124
233
  return "high";
125
234
  }
126
- if (/<(style|link|meta)/i.test(input) || /expression\s*\(/i.test(input)) {
235
+ if (DANGEROUS_CODE_PATTERN.test(input) || /<\s*(?:link|meta|base)\b/i.test(input)) {
127
236
  return "medium";
128
237
  }
129
- if (/<[^>]*>/i.test(input)) {
130
- return "low";
131
- }
132
238
  return "none";
133
239
  }
134
240
  function isDangerous(input) {
135
241
  if (!input) return false;
136
- try {
137
- return DETECT_DANGEROUS_TAGS.test(input) || DETECT_EVENT_HANDLERS.test(input) || DETECT_DANGEROUS_PROTOCOLS.test(input) || DETECT_DANGEROUS_ATTRIBUTES.test(input) || DETECT_TEMPLATE_INJECTION.test(input) || DETECT_POLYGLOT_JAVASCRIPT.test(input) || DETECT_POLYGLOT_EVENTS.test(input) || DANGEROUS_PROTOCOL_TOKEN.test(input);
138
- } catch {
139
- return true;
140
- }
242
+ return DANGEROUS_CONTAINER_PATTERN.test(input) || EVENT_HANDLER_PATTERN.test(input) || DANGEROUS_PROTOCOL_PATTERN.test(input) || DANGEROUS_CODE_PATTERN.test(input);
141
243
  }
142
244
  function sanitize(input, options) {
143
245
  const config = { ...DEFAULT_OPTIONS, ...options };
144
- if (input === null || input === void 0) return "";
145
- let str;
146
- try {
147
- if (typeof input === "string") {
148
- str = input;
149
- } else if (typeof input === "object") {
150
- str = JSON.stringify(input);
151
- } else {
152
- str = String(input);
153
- }
154
- } catch {
155
- return "";
156
- }
157
- if (str.length > config.maxLength) {
158
- str = str.substring(0, config.maxLength);
246
+ let str = coerceInput(input);
247
+ const maxLength = resolveMaxLength(config.maxLength);
248
+ if (str.length > maxLength) str = str.slice(0, maxLength);
249
+ if (!str) return "";
250
+ if (!str.includes("<") && !str.includes(">") && !HAS_CONTROL_CHARS.test(str) && !ENCODED_SYNTAX.test(str)) {
251
+ return str.replace(/\s+/g, " ").trim();
159
252
  }
160
- if (!str.trim()) return "";
161
- let result = str.replace(NULL_CONTROL_CHARS, "");
162
253
  try {
163
- result = decodeEncodingBypasses(result);
164
- const dangerous = isDangerous(str) || isDangerous(result);
165
- if (!dangerous) {
166
- const benign = removeDangerousTagsWithContent(result).replace(HTML_TAG, "").replace(DANGEROUS_ATTRIBUTES, "").replace(TEMPLATE_INJECTION, "").replace(DANGEROUS_FUNCTIONS, "");
167
- return escapeAngleBrackets(benign).replace(/\s+/g, " ").trim();
168
- }
169
- result = removeDangerousTagsWithContent(result);
170
- result = handlePolyglotAttacks(result);
171
- let previous;
172
- let passes = 0;
173
- do {
174
- previous = result;
175
- result = result.replace(DANGEROUS_TAGS_WITH_CONTENT, "").replace(EVENT_HANDLERS_ENHANCED, "").replace(DANGEROUS_PROTOCOLS_ENHANCED, (match) => {
176
- const safeProtocols = config.allowedProtocols.join("|");
177
- return match.match(new RegExp(`^(?:href|src)\\s*=\\s*["']?\\s*(?:${safeProtocols}):\\/\\/`, "i")) ? match : "";
178
- }).replace(DANGEROUS_ATTRIBUTES, "").replace(TEMPLATE_INJECTION, "").replace(DANGEROUS_FUNCTIONS, "");
179
- passes++;
180
- } while (result !== previous && passes < MAX_FILTER_PASSES);
181
- if (config.aggressiveMode && SUSPICIOUS_PATTERNS.test(result)) {
182
- const tempCheck = result.replace(/<[^>]*>/g, "");
183
- if (SUSPICIOUS_PATTERNS.test(tempCheck)) {
184
- result = result.replace(/<[^>]*>/g, "").replace(/[<>]/g, "").replace(/javascript|vbscript/gi, "").replace(/\bon[a-z]+\s*=/gi, "").replace(/svg|SVG|sVg/gi, "").replace(/script|SCRIPT/gi, "").replace(/alert|eval/gi, "").replace(/\\x\w{2}/gi, "").replace(/[(){}[\]]/g, "");
185
- }
186
- }
187
- result = result.replace(/\s+/g, " ").trim();
188
- if (config.aggressiveMode && !HAS_TEXT_CONTENT.test(result)) {
189
- return "";
190
- }
191
- return escapeAngleBrackets(result);
254
+ const decoded = decodeEncodingBypasses(str.replace(NULL_CONTROL_CHARS, "")).replace(NULL_CONTROL_CHARS, "");
255
+ return stripToText(decoded);
192
256
  } catch {
193
- return str.replace(NULL_CONTROL_CHARS, "").replace(/<[^>]*>/g, "").replace(/[<>]/g, "").replace(/javascript|vbscript|eval|alert/gi, "").replace(/\bon[a-z]+\s*=/gi, "").replace(/\s+/g, " ").trim();
257
+ return escapeAngleBrackets(str.replace(NULL_CONTROL_CHARS, "")).replace(/\s+/g, " ").trim();
194
258
  }
195
259
  }
196
260
  function analyze(input, options) {
197
261
  const startTime = performance.now();
198
- const originalStr = String(input ?? "");
199
- const sanitized = sanitize(input, options);
262
+ const config = { ...DEFAULT_OPTIONS, ...options };
263
+ const originalStr = coerceInput(input).slice(0, resolveMaxLength(config.maxLength));
264
+ const sanitized = sanitize(originalStr, options);
200
265
  const processingTime = performance.now() - startTime;
201
- const hadThreats = sanitized !== originalStr;
202
- const threatLevel = assessThreatLevel(originalStr);
266
+ const decodedForAnalysis = decodeEncodingBypasses(originalStr.replace(NULL_CONTROL_CHARS, ""));
267
+ const threatLevel = assessThreatLevel(decodedForAnalysis);
268
+ const hadThreats = isDangerous(originalStr) || isDangerous(decodedForAnalysis);
203
269
  return {
204
270
  content: sanitized,
205
271
  hadThreats,
@@ -243,14 +309,17 @@ function escapeUrl(input, options) {
243
309
  const str = (typeof input === "string" ? input : String(input)).trim();
244
310
  if (!str) return "";
245
311
  const normalized = str.replace(/[\x00-\x20\x7F-\x9F]/g, "");
312
+ if (normalized.startsWith("//") || normalized.startsWith("\\\\")) return "";
313
+ const configuredProtocols = Array.isArray(config.allowedProtocols) ? config.allowedProtocols : [];
314
+ const requestedProtocols = new Set(
315
+ configuredProtocols.filter((protocol) => typeof protocol === "string").map((protocol) => protocol.toLowerCase()).filter((protocol) => SAFE_URL_PROTOCOLS.has(protocol))
316
+ );
246
317
  const colon = normalized.indexOf(":");
247
318
  if (colon !== -1) {
248
319
  const beforeColon = normalized.slice(0, colon);
249
320
  if (!/[/?#]/.test(beforeColon)) {
250
321
  const protocol = beforeColon.toLowerCase();
251
- if (!config.allowedProtocols.map((p) => p.toLowerCase()).includes(protocol)) {
252
- return "";
253
- }
322
+ if (!requestedProtocols.has(protocol)) return "";
254
323
  }
255
324
  }
256
325
  return escapeAttribute(normalized);
@@ -261,17 +330,17 @@ function getVersion() {
261
330
  function getStats() {
262
331
  return {
263
332
  version: getVersion(),
264
- securityLevel: "100% XSS Protection",
265
- performance: "Optimized for high-throughput"
333
+ securityLevel: "Plain-text output with contextual encoders",
334
+ performance: "Measure with the included benchmark on your target runtime"
266
335
  };
267
336
  }
268
337
  var Purifai = class {
269
338
  /**
270
- * Sanitize input with maximum security protection
339
+ * Convert HTML-like input to plain text
271
340
  *
272
341
  * @param input - Content to sanitize (string, object, or any type)
273
342
  * @param options - Optional configuration
274
- * @returns Sanitized string safe for HTML output
343
+ * @returns Plain-text string; render it through normal text interpolation
275
344
  *
276
345
  * @example
277
346
  * ```typescript
@@ -396,11 +465,11 @@ var index_default = Purifai;
396
465
  sanitizeBatch
397
466
  });
398
467
  /**
399
- * 🛡️ PURIFAI - Ultra-Secure HTML Sanitizer
468
+ * PURIFAI - bounded strip-to-text sanitizer and contextual encoders.
400
469
  *
401
- * Advanced XSS protection with polyglot attack resistance.
402
- * Blocks sophisticated obfuscation techniques that bypass other sanitizers.
470
+ * `sanitize()` removes markup; it does not preserve safe HTML. Use `escape()`,
471
+ * `escapeAttribute()`, or `escapeUrl()` when the destination context is known.
403
472
  *
404
- * @version 2.0.2
473
+ * @version 2.0.3
405
474
  * @license MIT
406
475
  */
package/dist/index.d.cts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
- * 🛡️ PURIFAI - Ultra-Secure HTML Sanitizer
2
+ * PURIFAI - bounded strip-to-text sanitizer and contextual encoders.
3
3
  *
4
- * Advanced XSS protection with polyglot attack resistance.
5
- * Blocks sophisticated obfuscation techniques that bypass other sanitizers.
4
+ * `sanitize()` removes markup; it does not preserve safe HTML. Use `escape()`,
5
+ * `escapeAttribute()`, or `escapeUrl()` when the destination context is known.
6
6
  *
7
- * @version 2.0.2
7
+ * @version 2.0.3
8
8
  * @license MIT
9
9
  */
10
10
  /**
@@ -13,9 +13,9 @@
13
13
  export interface PurifaiOptions {
14
14
  /** Maximum input length (default: 1MB) */
15
15
  maxLength?: number;
16
- /** Custom allowed protocols (default: ['http', 'https', 'mailto']) */
16
+ /** Subset of the built-in safe URL protocols (default: http, https, mailto) */
17
17
  allowedProtocols?: string[];
18
- /** Enable aggressive mode for maximum security (default: true) */
18
+ /** @deprecated Retained for source compatibility; strip-to-text is always used. */
19
19
  aggressiveMode?: boolean;
20
20
  }
21
21
  /**
@@ -24,7 +24,7 @@ export interface PurifaiOptions {
24
24
  export interface PurifaiResult {
25
25
  /** Sanitized content */
26
26
  content: string;
27
- /** Whether dangerous content was detected */
27
+ /** Advisory: whether a known dangerous pattern was detected */
28
28
  hadThreats: boolean;
29
29
  /** Processing time in milliseconds */
30
30
  processingTime: number;
@@ -36,7 +36,7 @@ export interface PurifaiResult {
36
36
  */
37
37
  declare function isDangerous(input: string): boolean;
38
38
  /**
39
- * Sanitize input with maximum security protection
39
+ * Convert HTML-like input to inert plain text using a bounded forward scanner.
40
40
  */
41
41
  declare function sanitize(input: unknown, options?: PurifaiOptions): string;
42
42
  /**
@@ -50,10 +50,8 @@ declare function sanitizeBatch(inputs: unknown[], options?: PurifaiOptions): str
50
50
  /**
51
51
  * Escape text for insertion into an HTML body context.
52
52
  *
53
- * Lossless and total: nothing is removed, so this is the correct choice when
54
- * the input is plain text rather than markup. `sanitize()` cannot tell the two
55
- * apart — `a<b && c>d` is a valid HTML start tag by the parsing spec, so
56
- * `sanitize()` drops it while `escape()` preserves it verbatim.
53
+ * Lossless apart from control characters: this is the correct choice when the
54
+ * input is plain text rather than markup and exact text fidelity matters.
57
55
  */
58
56
  declare function escape(input: unknown): string;
59
57
  /**
@@ -68,16 +66,16 @@ declare function escapeAttribute(input: unknown): string;
68
66
  /**
69
67
  * Escape and validate a value used as a URL.
70
68
  *
71
- * Returns '' when the protocol is not in `allowedProtocols`, which is what
72
- * stops `javascript:`, `data:` and friends from reaching an `href`. Relative
73
- * URLs are allowed through, since they cannot carry a protocol.
69
+ * Returns '' when the protocol is not in the caller-selected subset of the
70
+ * built-in safe protocols. Script-bearing schemes and protocol-relative URLs
71
+ * cannot be enabled through options.
74
72
  */
75
73
  declare function escapeUrl(input: unknown, options?: PurifaiOptions): string;
76
74
  /**
77
- * 🛡️ Purifai - Ultra-Secure HTML Sanitizer
75
+ * Purifai - bounded strip-to-text sanitizer
78
76
  *
79
- * Advanced lightweight HTML sanitizer with superior XSS protection
80
- * against known attack vectors including advanced polyglot attacks.
77
+ * Converts HTML-like input to plain text and exposes separate contextual
78
+ * encoders for HTML text, attributes, and URLs.
81
79
  *
82
80
  * Every method delegates to a module-level function and reads no instance or
83
81
  * class state, so `Purifai.sanitize` and the standalone `sanitize` export are
@@ -85,11 +83,11 @@ declare function escapeUrl(input: unknown, options?: PurifaiOptions): string;
85
83
  */
86
84
  export declare class Purifai {
87
85
  /**
88
- * Sanitize input with maximum security protection
86
+ * Convert HTML-like input to plain text
89
87
  *
90
88
  * @param input - Content to sanitize (string, object, or any type)
91
89
  * @param options - Optional configuration
92
- * @returns Sanitized string safe for HTML output
90
+ * @returns Plain-text string; render it through normal text interpolation
93
91
  *
94
92
  * @example
95
93
  * ```typescript
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
- * 🛡️ PURIFAI - Ultra-Secure HTML Sanitizer
2
+ * PURIFAI - bounded strip-to-text sanitizer and contextual encoders.
3
3
  *
4
- * Advanced XSS protection with polyglot attack resistance.
5
- * Blocks sophisticated obfuscation techniques that bypass other sanitizers.
4
+ * `sanitize()` removes markup; it does not preserve safe HTML. Use `escape()`,
5
+ * `escapeAttribute()`, or `escapeUrl()` when the destination context is known.
6
6
  *
7
- * @version 2.0.2
7
+ * @version 2.0.3
8
8
  * @license MIT
9
9
  */
10
10
  /**
@@ -13,9 +13,9 @@
13
13
  export interface PurifaiOptions {
14
14
  /** Maximum input length (default: 1MB) */
15
15
  maxLength?: number;
16
- /** Custom allowed protocols (default: ['http', 'https', 'mailto']) */
16
+ /** Subset of the built-in safe URL protocols (default: http, https, mailto) */
17
17
  allowedProtocols?: string[];
18
- /** Enable aggressive mode for maximum security (default: true) */
18
+ /** @deprecated Retained for source compatibility; strip-to-text is always used. */
19
19
  aggressiveMode?: boolean;
20
20
  }
21
21
  /**
@@ -24,7 +24,7 @@ export interface PurifaiOptions {
24
24
  export interface PurifaiResult {
25
25
  /** Sanitized content */
26
26
  content: string;
27
- /** Whether dangerous content was detected */
27
+ /** Advisory: whether a known dangerous pattern was detected */
28
28
  hadThreats: boolean;
29
29
  /** Processing time in milliseconds */
30
30
  processingTime: number;
@@ -36,7 +36,7 @@ export interface PurifaiResult {
36
36
  */
37
37
  declare function isDangerous(input: string): boolean;
38
38
  /**
39
- * Sanitize input with maximum security protection
39
+ * Convert HTML-like input to inert plain text using a bounded forward scanner.
40
40
  */
41
41
  declare function sanitize(input: unknown, options?: PurifaiOptions): string;
42
42
  /**
@@ -50,10 +50,8 @@ declare function sanitizeBatch(inputs: unknown[], options?: PurifaiOptions): str
50
50
  /**
51
51
  * Escape text for insertion into an HTML body context.
52
52
  *
53
- * Lossless and total: nothing is removed, so this is the correct choice when
54
- * the input is plain text rather than markup. `sanitize()` cannot tell the two
55
- * apart — `a<b && c>d` is a valid HTML start tag by the parsing spec, so
56
- * `sanitize()` drops it while `escape()` preserves it verbatim.
53
+ * Lossless apart from control characters: this is the correct choice when the
54
+ * input is plain text rather than markup and exact text fidelity matters.
57
55
  */
58
56
  declare function escape(input: unknown): string;
59
57
  /**
@@ -68,16 +66,16 @@ declare function escapeAttribute(input: unknown): string;
68
66
  /**
69
67
  * Escape and validate a value used as a URL.
70
68
  *
71
- * Returns '' when the protocol is not in `allowedProtocols`, which is what
72
- * stops `javascript:`, `data:` and friends from reaching an `href`. Relative
73
- * URLs are allowed through, since they cannot carry a protocol.
69
+ * Returns '' when the protocol is not in the caller-selected subset of the
70
+ * built-in safe protocols. Script-bearing schemes and protocol-relative URLs
71
+ * cannot be enabled through options.
74
72
  */
75
73
  declare function escapeUrl(input: unknown, options?: PurifaiOptions): string;
76
74
  /**
77
- * 🛡️ Purifai - Ultra-Secure HTML Sanitizer
75
+ * Purifai - bounded strip-to-text sanitizer
78
76
  *
79
- * Advanced lightweight HTML sanitizer with superior XSS protection
80
- * against known attack vectors including advanced polyglot attacks.
77
+ * Converts HTML-like input to plain text and exposes separate contextual
78
+ * encoders for HTML text, attributes, and URLs.
81
79
  *
82
80
  * Every method delegates to a module-level function and reads no instance or
83
81
  * class state, so `Purifai.sanitize` and the standalone `sanitize` export are
@@ -85,11 +83,11 @@ declare function escapeUrl(input: unknown, options?: PurifaiOptions): string;
85
83
  */
86
84
  export declare class Purifai {
87
85
  /**
88
- * Sanitize input with maximum security protection
86
+ * Convert HTML-like input to plain text
89
87
  *
90
88
  * @param input - Content to sanitize (string, object, or any type)
91
89
  * @param options - Optional configuration
92
- * @returns Sanitized string safe for HTML output
90
+ * @returns Plain-text string; render it through normal text interpolation
93
91
  *
94
92
  * @example
95
93
  * ```typescript