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/README.md +119 -104
- package/dist/index.cjs +206 -137
- package/dist/index.d.cts +18 -20
- package/dist/index.d.ts +18 -20
- package/dist/index.js +206 -137
- package/package.json +8 -4
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
|
|
42
|
-
var
|
|
43
|
-
var
|
|
44
|
-
var
|
|
45
|
-
var
|
|
46
|
-
var
|
|
47
|
-
var
|
|
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, "<").replace(/>/g, ">");
|
|
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.
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
146
|
+
return null;
|
|
91
147
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
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
|
|
96
|
-
|
|
211
|
+
function coerceInput(input) {
|
|
212
|
+
if (input === null || input === void 0) return "";
|
|
97
213
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
221
|
+
return "";
|
|
115
222
|
}
|
|
116
|
-
|
|
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 (
|
|
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 (
|
|
232
|
+
if (DANGEROUS_CONTAINER_PATTERN.test(input) || /\beval\s*\(/i.test(input)) {
|
|
124
233
|
return "high";
|
|
125
234
|
}
|
|
126
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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, "")
|
|
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
|
|
199
|
-
const
|
|
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
|
|
202
|
-
const threatLevel = assessThreatLevel(
|
|
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 (!
|
|
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: "
|
|
265
|
-
performance: "
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
468
|
+
* PURIFAI - bounded strip-to-text sanitizer and contextual encoders.
|
|
400
469
|
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
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.
|
|
473
|
+
* @version 2.0.3
|
|
405
474
|
* @license MIT
|
|
406
475
|
*/
|
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* PURIFAI - bounded strip-to-text sanitizer and contextual encoders.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
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
|
-
/**
|
|
16
|
+
/** Subset of the built-in safe URL protocols (default: http, https, mailto) */
|
|
17
17
|
allowedProtocols?: string[];
|
|
18
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
|
54
|
-
*
|
|
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
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
-
*
|
|
75
|
+
* Purifai - bounded strip-to-text sanitizer
|
|
78
76
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
2
|
+
* PURIFAI - bounded strip-to-text sanitizer and contextual encoders.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
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
|
-
/**
|
|
16
|
+
/** Subset of the built-in safe URL protocols (default: http, https, mailto) */
|
|
17
17
|
allowedProtocols?: string[];
|
|
18
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
|
54
|
-
*
|
|
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
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
-
*
|
|
75
|
+
* Purifai - bounded strip-to-text sanitizer
|
|
78
76
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
-
*
|
|
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
|
|
90
|
+
* @returns Plain-text string; render it through normal text interpolation
|
|
93
91
|
*
|
|
94
92
|
* @example
|
|
95
93
|
* ```typescript
|