purifai 1.0.0 → 2.0.0
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 +81 -49
- package/dist/index.cjs +282 -159
- package/dist/index.d.cts +80 -25
- package/dist/index.d.ts +80 -25
- package/dist/index.js +279 -159
- package/package.json +28 -11
package/dist/index.d.ts
CHANGED
|
@@ -4,17 +4,15 @@
|
|
|
4
4
|
* Advanced XSS protection with polyglot attack resistance.
|
|
5
5
|
* Blocks sophisticated obfuscation techniques that bypass other sanitizers.
|
|
6
6
|
*
|
|
7
|
-
* @version
|
|
7
|
+
* @version 2.0.0
|
|
8
8
|
* @license MIT
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
11
|
* Purifai Configuration Options
|
|
12
12
|
*/
|
|
13
|
-
interface PurifaiOptions {
|
|
13
|
+
export interface PurifaiOptions {
|
|
14
14
|
/** Maximum input length (default: 1MB) */
|
|
15
15
|
maxLength?: number;
|
|
16
|
-
/** Allow safe HTML tags like <b>, <i>, <p> (default: false for maximum security) */
|
|
17
|
-
allowBasicHtml?: boolean;
|
|
18
16
|
/** Custom allowed protocols (default: ['http', 'https', 'mailto']) */
|
|
19
17
|
allowedProtocols?: string[];
|
|
20
18
|
/** Enable aggressive mode for maximum security (default: true) */
|
|
@@ -23,7 +21,7 @@ interface PurifaiOptions {
|
|
|
23
21
|
/**
|
|
24
22
|
* Purifai Sanitization Result
|
|
25
23
|
*/
|
|
26
|
-
interface PurifaiResult {
|
|
24
|
+
export interface PurifaiResult {
|
|
27
25
|
/** Sanitized content */
|
|
28
26
|
content: string;
|
|
29
27
|
/** Whether dangerous content was detected */
|
|
@@ -33,14 +31,59 @@ interface PurifaiResult {
|
|
|
33
31
|
/** Threat level: 'none' | 'low' | 'medium' | 'high' | 'critical' */
|
|
34
32
|
threatLevel: 'none' | 'low' | 'medium' | 'high' | 'critical';
|
|
35
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if input contains dangerous patterns
|
|
36
|
+
*/
|
|
37
|
+
declare function isDangerous(input: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Sanitize input with maximum security protection
|
|
40
|
+
*/
|
|
41
|
+
declare function sanitize(input: unknown, options?: PurifaiOptions): string;
|
|
42
|
+
/**
|
|
43
|
+
* Sanitize input and return detailed analysis
|
|
44
|
+
*/
|
|
45
|
+
declare function analyze(input: unknown, options?: PurifaiOptions): PurifaiResult;
|
|
46
|
+
/**
|
|
47
|
+
* Batch sanitize multiple inputs for optimal performance
|
|
48
|
+
*/
|
|
49
|
+
declare function sanitizeBatch(inputs: unknown[], options?: PurifaiOptions): string[];
|
|
50
|
+
/**
|
|
51
|
+
* Escape text for insertion into an HTML body context.
|
|
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.
|
|
57
|
+
*/
|
|
58
|
+
declare function escape(input: unknown): string;
|
|
59
|
+
/**
|
|
60
|
+
* Escape text for insertion into an HTML attribute value.
|
|
61
|
+
*
|
|
62
|
+
* Stricter than `escape()`: every character outside `[a-zA-Z0-9]` is
|
|
63
|
+
* hex-encoded, which stays safe even in an unquoted attribute — the case that
|
|
64
|
+
* breaks naive escaping, since a bare space or backtick can end the value and
|
|
65
|
+
* start a new attribute such as `onerror=`.
|
|
66
|
+
*/
|
|
67
|
+
declare function escapeAttribute(input: unknown): string;
|
|
68
|
+
/**
|
|
69
|
+
* Escape and validate a value used as a URL.
|
|
70
|
+
*
|
|
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.
|
|
74
|
+
*/
|
|
75
|
+
declare function escapeUrl(input: unknown, options?: PurifaiOptions): string;
|
|
36
76
|
/**
|
|
37
77
|
* 🛡️ Purifai - Ultra-Secure HTML Sanitizer
|
|
38
78
|
*
|
|
39
79
|
* Advanced lightweight HTML sanitizer with superior XSS protection
|
|
40
80
|
* against known attack vectors including advanced polyglot attacks.
|
|
81
|
+
*
|
|
82
|
+
* Every method delegates to a module-level function and reads no instance or
|
|
83
|
+
* class state, so `Purifai.sanitize` and the standalone `sanitize` export are
|
|
84
|
+
* interchangeable and neither depends on its call-site receiver.
|
|
41
85
|
*/
|
|
42
|
-
declare class Purifai {
|
|
43
|
-
private static defaultOptions;
|
|
86
|
+
export declare class Purifai {
|
|
44
87
|
/**
|
|
45
88
|
* Sanitize input with maximum security protection
|
|
46
89
|
*
|
|
@@ -73,18 +116,6 @@ declare class Purifai {
|
|
|
73
116
|
* ```
|
|
74
117
|
*/
|
|
75
118
|
static analyze(input: unknown, options?: PurifaiOptions): PurifaiResult;
|
|
76
|
-
/**
|
|
77
|
-
* Decode common encoding bypasses
|
|
78
|
-
*/
|
|
79
|
-
private static decodeEncodingBypasses;
|
|
80
|
-
/**
|
|
81
|
-
* Enhanced polyglot attack handling - Addresses sophisticated XSS techniques
|
|
82
|
-
*/
|
|
83
|
-
private static handlePolyglotAttacks;
|
|
84
|
-
/**
|
|
85
|
-
* Assess threat level of input content
|
|
86
|
-
*/
|
|
87
|
-
private static assessThreatLevel;
|
|
88
119
|
/**
|
|
89
120
|
* Check if input contains dangerous patterns
|
|
90
121
|
*
|
|
@@ -115,6 +146,34 @@ declare class Purifai {
|
|
|
115
146
|
* ```
|
|
116
147
|
*/
|
|
117
148
|
static sanitizeBatch(inputs: unknown[], options?: PurifaiOptions): string[];
|
|
149
|
+
/**
|
|
150
|
+
* Escape text for an HTML body context (lossless — nothing is removed)
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* Purifai.escape('if (a<b && c>d)'); // "if (a<b && c>d)"
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
static escape(input: unknown): string;
|
|
158
|
+
/**
|
|
159
|
+
* Escape text for an HTML attribute value, safe even when unquoted
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* `<div title="${Purifai.escapeAttribute(userInput)}">`
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
static escapeAttribute(input: unknown): string;
|
|
167
|
+
/**
|
|
168
|
+
* Escape a URL, returning '' if its protocol is not allowed
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* Purifai.escapeUrl('javascript:alert(1)'); // ""
|
|
173
|
+
* Purifai.escapeUrl('https://example.com'); // escaped, safe for href
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
static escapeUrl(input: unknown, options?: PurifaiOptions): string;
|
|
118
177
|
/**
|
|
119
178
|
* Get version information
|
|
120
179
|
*/
|
|
@@ -128,9 +187,5 @@ declare class Purifai {
|
|
|
128
187
|
performance: string;
|
|
129
188
|
};
|
|
130
189
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
declare const isDangerous: typeof Purifai.isDangerous;
|
|
134
|
-
declare const sanitizeBatch: typeof Purifai.sanitizeBatch;
|
|
135
|
-
|
|
136
|
-
export { Purifai, type PurifaiOptions, type PurifaiResult, analyze, Purifai as default, isDangerous, sanitize, sanitizeBatch };
|
|
190
|
+
export { sanitize, analyze, isDangerous, sanitizeBatch, escape, escapeAttribute, escapeUrl };
|
|
191
|
+
export default Purifai;
|
package/dist/index.js
CHANGED
|
@@ -9,68 +9,256 @@ var NULL_CONTROL_CHARS = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g;
|
|
|
9
9
|
var POLYGLOT_JAVASCRIPT = /j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*:/gi;
|
|
10
10
|
var POLYGLOT_EVENTS = /o\s*n\s*[a-z]+\s*(?:=|alert)/gi;
|
|
11
11
|
var ENCODED_TAGS = /\\x3c|\\x3e|%3c|%3e/gi;
|
|
12
|
-
var SUSPICIOUS_PATTERNS = /<|>|javascript:|vbscript
|
|
12
|
+
var SUSPICIOUS_PATTERNS = /<|>|javascript:|vbscript:|\bon[a-z]+\s*=|@import|\{\{|<%|<\?|\${|#\{/i;
|
|
13
|
+
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;
|
|
14
|
+
var HAS_TEXT_CONTENT = /[a-z0-9]/i;
|
|
15
|
+
var HTML_TAG = /<\/?[a-zA-Z][^>]*>/g;
|
|
16
|
+
function escapeAngleBrackets(str) {
|
|
17
|
+
return str.replace(/</g, "<").replace(/>/g, ">");
|
|
18
|
+
}
|
|
19
|
+
function stateless(pattern) {
|
|
20
|
+
return new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, ""));
|
|
21
|
+
}
|
|
22
|
+
var DETECT_DANGEROUS_TAGS = stateless(DANGEROUS_TAGS_WITH_CONTENT);
|
|
23
|
+
var DETECT_EVENT_HANDLERS = stateless(EVENT_HANDLERS_ENHANCED);
|
|
24
|
+
var DETECT_DANGEROUS_PROTOCOLS = stateless(DANGEROUS_PROTOCOLS_ENHANCED);
|
|
25
|
+
var DETECT_DANGEROUS_ATTRIBUTES = stateless(DANGEROUS_ATTRIBUTES);
|
|
26
|
+
var DETECT_TEMPLATE_INJECTION = stateless(TEMPLATE_INJECTION);
|
|
27
|
+
var DETECT_POLYGLOT_JAVASCRIPT = stateless(POLYGLOT_JAVASCRIPT);
|
|
28
|
+
var DETECT_POLYGLOT_EVENTS = stateless(POLYGLOT_EVENTS);
|
|
29
|
+
var DEFAULT_OPTIONS = {
|
|
30
|
+
maxLength: 1e6,
|
|
31
|
+
// 1MB
|
|
32
|
+
allowedProtocols: ["http", "https", "mailto"],
|
|
33
|
+
aggressiveMode: true
|
|
34
|
+
};
|
|
35
|
+
var VERSION = "2.0.0";
|
|
36
|
+
var MAX_FILTER_PASSES = 5;
|
|
37
|
+
function decodeEncodingBypasses(str) {
|
|
38
|
+
let result = str;
|
|
39
|
+
try {
|
|
40
|
+
result = decodeURIComponent(result);
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
result = result.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => {
|
|
45
|
+
return String.fromCharCode(parseInt(hex, 16));
|
|
46
|
+
});
|
|
47
|
+
result = result.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => {
|
|
48
|
+
return String.fromCharCode(parseInt(hex, 16));
|
|
49
|
+
});
|
|
50
|
+
result = result.replace(/&#(\d+);/g, (_, num) => {
|
|
51
|
+
return String.fromCharCode(parseInt(num, 10));
|
|
52
|
+
});
|
|
53
|
+
result = result.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => {
|
|
54
|
+
return String.fromCharCode(parseInt(hex, 16));
|
|
55
|
+
});
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function removeDangerousTagsWithContent(str) {
|
|
61
|
+
return str.replace(DANGEROUS_TAGS_WITH_CONTENT, "");
|
|
62
|
+
}
|
|
63
|
+
function handlePolyglotAttacks(str) {
|
|
64
|
+
let result = str;
|
|
65
|
+
try {
|
|
66
|
+
result = result.replace(POLYGLOT_JAVASCRIPT, "");
|
|
67
|
+
result = result.replace(POLYGLOT_EVENTS, "");
|
|
68
|
+
result = result.replace(ENCODED_TAGS, "");
|
|
69
|
+
result = result.replace(/javascript\s*:\s*\/\*[\s\S]*?\*\//gi, "");
|
|
70
|
+
result = result.replace(/javascript\s*:\s*\/\*[^*]*\*\/[^>]*/gi, "");
|
|
71
|
+
result = result.replace(/<form[^>]*>[\s\S]*?<\/form>/gi, "");
|
|
72
|
+
result = result.replace(/<math[^>]*>[\s\S]*?<\/math>/gi, "");
|
|
73
|
+
result = result.replace(/%0[AD]/gi, "");
|
|
74
|
+
if (/svg|sVg|\x3c|\\\w{3}/i.test(result)) {
|
|
75
|
+
result = result.replace(/svg[^>]*>/gi, "");
|
|
76
|
+
result = result.replace(/sVg[^>]*>/gi, "");
|
|
77
|
+
result = result.replace(/\\x\w{2}/gi, "");
|
|
78
|
+
result = result.replace(/<\/[^>]*>/gi, "");
|
|
79
|
+
result = result.replace(/<[^>]*>/gi, "");
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
result = result.replace(/<[^>]*>/g, "").replace(/[<>]/g, "");
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
function assessThreatLevel(input) {
|
|
87
|
+
if (!input) return "none";
|
|
88
|
+
if (/<script/i.test(input) || /javascript\s*:/i.test(input) || /\bon[a-z]+\s*=/i.test(input)) {
|
|
89
|
+
return "critical";
|
|
90
|
+
}
|
|
91
|
+
if (/<(iframe|object|embed|form|svg|math)/i.test(input) || /eval\s*\(/i.test(input)) {
|
|
92
|
+
return "high";
|
|
93
|
+
}
|
|
94
|
+
if (/<(style|link|meta)/i.test(input) || /expression\s*\(/i.test(input)) {
|
|
95
|
+
return "medium";
|
|
96
|
+
}
|
|
97
|
+
if (/<[^>]*>/i.test(input)) {
|
|
98
|
+
return "low";
|
|
99
|
+
}
|
|
100
|
+
return "none";
|
|
101
|
+
}
|
|
102
|
+
function isDangerous(input) {
|
|
103
|
+
if (!input) return false;
|
|
104
|
+
try {
|
|
105
|
+
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);
|
|
106
|
+
} catch {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function sanitize(input, options) {
|
|
111
|
+
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
112
|
+
if (input === null || input === void 0) return "";
|
|
113
|
+
let str;
|
|
114
|
+
try {
|
|
115
|
+
if (typeof input === "string") {
|
|
116
|
+
str = input;
|
|
117
|
+
} else if (typeof input === "object") {
|
|
118
|
+
str = JSON.stringify(input);
|
|
119
|
+
} else {
|
|
120
|
+
str = String(input);
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
if (str.length > config.maxLength) {
|
|
126
|
+
str = str.substring(0, config.maxLength);
|
|
127
|
+
}
|
|
128
|
+
if (!str.trim()) return "";
|
|
129
|
+
let result = str.replace(NULL_CONTROL_CHARS, "");
|
|
130
|
+
try {
|
|
131
|
+
result = decodeEncodingBypasses(result);
|
|
132
|
+
const dangerous = isDangerous(str) || isDangerous(result);
|
|
133
|
+
if (!dangerous) {
|
|
134
|
+
const benign = removeDangerousTagsWithContent(result).replace(HTML_TAG, "").replace(DANGEROUS_ATTRIBUTES, "").replace(TEMPLATE_INJECTION, "").replace(DANGEROUS_FUNCTIONS, "");
|
|
135
|
+
return escapeAngleBrackets(benign).replace(/\s+/g, " ").trim();
|
|
136
|
+
}
|
|
137
|
+
result = removeDangerousTagsWithContent(result);
|
|
138
|
+
result = handlePolyglotAttacks(result);
|
|
139
|
+
let previous;
|
|
140
|
+
let passes = 0;
|
|
141
|
+
do {
|
|
142
|
+
previous = result;
|
|
143
|
+
result = result.replace(DANGEROUS_TAGS_WITH_CONTENT, "").replace(EVENT_HANDLERS_ENHANCED, "").replace(DANGEROUS_PROTOCOLS_ENHANCED, (match) => {
|
|
144
|
+
const safeProtocols = config.allowedProtocols.join("|");
|
|
145
|
+
return match.match(new RegExp(`^(?:href|src)\\s*=\\s*["']?\\s*(?:${safeProtocols}):\\/\\/`, "i")) ? match : "";
|
|
146
|
+
}).replace(DANGEROUS_ATTRIBUTES, "").replace(TEMPLATE_INJECTION, "").replace(DANGEROUS_FUNCTIONS, "");
|
|
147
|
+
passes++;
|
|
148
|
+
} while (result !== previous && passes < MAX_FILTER_PASSES);
|
|
149
|
+
if (config.aggressiveMode && SUSPICIOUS_PATTERNS.test(result)) {
|
|
150
|
+
const tempCheck = result.replace(/<[^>]*>/g, "");
|
|
151
|
+
if (SUSPICIOUS_PATTERNS.test(tempCheck)) {
|
|
152
|
+
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, "");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
result = result.replace(/\s+/g, " ").trim();
|
|
156
|
+
if (config.aggressiveMode && !HAS_TEXT_CONTENT.test(result)) {
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
return escapeAngleBrackets(result);
|
|
160
|
+
} catch {
|
|
161
|
+
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();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function analyze(input, options) {
|
|
165
|
+
const startTime = performance.now();
|
|
166
|
+
const originalStr = String(input ?? "");
|
|
167
|
+
const sanitized = sanitize(input, options);
|
|
168
|
+
const processingTime = performance.now() - startTime;
|
|
169
|
+
const hadThreats = sanitized !== originalStr;
|
|
170
|
+
const threatLevel = assessThreatLevel(originalStr);
|
|
171
|
+
return {
|
|
172
|
+
content: sanitized,
|
|
173
|
+
hadThreats,
|
|
174
|
+
processingTime,
|
|
175
|
+
threatLevel
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function sanitizeBatch(inputs, options) {
|
|
179
|
+
return inputs.map((input) => {
|
|
180
|
+
try {
|
|
181
|
+
return sanitize(input, options);
|
|
182
|
+
} catch {
|
|
183
|
+
return "";
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function escape(input) {
|
|
188
|
+
if (input === null || input === void 0) return "";
|
|
189
|
+
const str = typeof input === "string" ? input : String(input);
|
|
190
|
+
return str.replace(NULL_CONTROL_CHARS, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
191
|
+
}
|
|
192
|
+
function escapeAttribute(input) {
|
|
193
|
+
if (input === null || input === void 0) return "";
|
|
194
|
+
const str = (typeof input === "string" ? input : String(input)).replace(NULL_CONTROL_CHARS, "");
|
|
195
|
+
let out = "";
|
|
196
|
+
for (const char of str) {
|
|
197
|
+
const code = char.codePointAt(0);
|
|
198
|
+
if (/[a-zA-Z0-9]/.test(char)) {
|
|
199
|
+
out += char;
|
|
200
|
+
} else if (code < 256) {
|
|
201
|
+
out += `&#x${code.toString(16).padStart(2, "0")};`;
|
|
202
|
+
} else {
|
|
203
|
+
out += `&#x${code.toString(16)};`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
function escapeUrl(input, options) {
|
|
209
|
+
if (input === null || input === void 0) return "";
|
|
210
|
+
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
211
|
+
const str = (typeof input === "string" ? input : String(input)).trim();
|
|
212
|
+
if (!str) return "";
|
|
213
|
+
const normalized = str.replace(/[\x00-\x20\x7F-\x9F]/g, "");
|
|
214
|
+
const colon = normalized.indexOf(":");
|
|
215
|
+
if (colon !== -1) {
|
|
216
|
+
const beforeColon = normalized.slice(0, colon);
|
|
217
|
+
if (!/[/?#]/.test(beforeColon)) {
|
|
218
|
+
const protocol = beforeColon.toLowerCase();
|
|
219
|
+
if (!config.allowedProtocols.map((p) => p.toLowerCase()).includes(protocol)) {
|
|
220
|
+
return "";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return escapeAttribute(normalized);
|
|
225
|
+
}
|
|
226
|
+
function getVersion() {
|
|
227
|
+
return VERSION;
|
|
228
|
+
}
|
|
229
|
+
function getStats() {
|
|
230
|
+
return {
|
|
231
|
+
version: getVersion(),
|
|
232
|
+
securityLevel: "100% XSS Protection",
|
|
233
|
+
performance: "Optimized for high-throughput"
|
|
234
|
+
};
|
|
235
|
+
}
|
|
13
236
|
var Purifai = class {
|
|
14
237
|
/**
|
|
15
238
|
* Sanitize input with maximum security protection
|
|
16
|
-
*
|
|
239
|
+
*
|
|
17
240
|
* @param input - Content to sanitize (string, object, or any type)
|
|
18
241
|
* @param options - Optional configuration
|
|
19
242
|
* @returns Sanitized string safe for HTML output
|
|
20
|
-
*
|
|
243
|
+
*
|
|
21
244
|
* @example
|
|
22
245
|
* ```typescript
|
|
23
246
|
* import { Purifai } from 'purifai';
|
|
24
|
-
*
|
|
247
|
+
*
|
|
25
248
|
* const clean = Purifai.sanitize('<script>alert("xss")</script>Hello World');
|
|
26
249
|
* console.log(clean); // "Hello World"
|
|
27
250
|
* ```
|
|
28
251
|
*/
|
|
29
252
|
static sanitize(input, options) {
|
|
30
|
-
|
|
31
|
-
if (input === null || input === void 0) return "";
|
|
32
|
-
let str;
|
|
33
|
-
try {
|
|
34
|
-
if (typeof input === "string") {
|
|
35
|
-
str = input;
|
|
36
|
-
} else if (typeof input === "object") {
|
|
37
|
-
str = JSON.stringify(input);
|
|
38
|
-
} else {
|
|
39
|
-
str = String(input);
|
|
40
|
-
}
|
|
41
|
-
} catch {
|
|
42
|
-
return "";
|
|
43
|
-
}
|
|
44
|
-
if (str.length > config.maxLength) {
|
|
45
|
-
str = str.substring(0, config.maxLength);
|
|
46
|
-
}
|
|
47
|
-
if (!str.trim()) return "";
|
|
48
|
-
let result = str.replace(NULL_CONTROL_CHARS, "");
|
|
49
|
-
try {
|
|
50
|
-
result = this.decodeEncodingBypasses(result);
|
|
51
|
-
result = this.handlePolyglotAttacks(result);
|
|
52
|
-
result = result.replace(DANGEROUS_TAGS_WITH_CONTENT, "").replace(EVENT_HANDLERS_ENHANCED, "").replace(DANGEROUS_PROTOCOLS_ENHANCED, (match) => {
|
|
53
|
-
const safeProtocols = config.allowedProtocols.join("|");
|
|
54
|
-
return match.match(new RegExp(`^(?:href|src)\\s*=\\s*["']?\\s*(?:${safeProtocols}):\\/\\/`, "i")) ? match : "";
|
|
55
|
-
}).replace(DANGEROUS_ATTRIBUTES, "").replace(TEMPLATE_INJECTION, "").replace(DANGEROUS_FUNCTIONS, "");
|
|
56
|
-
if (config.aggressiveMode && SUSPICIOUS_PATTERNS.test(result)) {
|
|
57
|
-
const tempCheck = result.replace(/<[^>]*>/g, "");
|
|
58
|
-
if (SUSPICIOUS_PATTERNS.test(tempCheck)) {
|
|
59
|
-
result = result.replace(/<[^>]*>/g, "").replace(/[<>]/g, "").replace(/javascript|vbscript/gi, "").replace(/on\w+\s*=/gi, "").replace(/svg|SVG|sVg/gi, "").replace(/script|SCRIPT/gi, "").replace(/alert|eval/gi, "").replace(/\\x\w{2}/gi, "").replace(/[(){}[\]]/g, "");
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return result.replace(/\s+/g, " ").trim();
|
|
63
|
-
} catch (error) {
|
|
64
|
-
return str.replace(NULL_CONTROL_CHARS, "").replace(/<[^>]*>/g, "").replace(/[<>]/g, "").replace(/javascript|vbscript|eval|alert/gi, "").replace(/on\w+\s*=/gi, "").replace(/\s+/g, " ").trim();
|
|
65
|
-
}
|
|
253
|
+
return sanitize(input, options);
|
|
66
254
|
}
|
|
67
255
|
/**
|
|
68
256
|
* Sanitize input and return detailed analysis
|
|
69
|
-
*
|
|
257
|
+
*
|
|
70
258
|
* @param input - Content to sanitize
|
|
71
259
|
* @param options - Optional configuration
|
|
72
260
|
* @returns Detailed sanitization result with threat analysis
|
|
73
|
-
*
|
|
261
|
+
*
|
|
74
262
|
* @example
|
|
75
263
|
* ```typescript
|
|
76
264
|
* const result = Purifai.analyze('<script>alert("xss")</script>Hello');
|
|
@@ -80,93 +268,14 @@ var Purifai = class {
|
|
|
80
268
|
* ```
|
|
81
269
|
*/
|
|
82
270
|
static analyze(input, options) {
|
|
83
|
-
|
|
84
|
-
const originalStr = String(input || "");
|
|
85
|
-
const sanitized = this.sanitize(input, options);
|
|
86
|
-
const processingTime = performance.now() - startTime;
|
|
87
|
-
const hadThreats = sanitized !== originalStr;
|
|
88
|
-
const threatLevel = this.assessThreatLevel(originalStr);
|
|
89
|
-
return {
|
|
90
|
-
content: sanitized,
|
|
91
|
-
hadThreats,
|
|
92
|
-
processingTime,
|
|
93
|
-
threatLevel
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Decode common encoding bypasses
|
|
98
|
-
*/
|
|
99
|
-
static decodeEncodingBypasses(str) {
|
|
100
|
-
let result = str;
|
|
101
|
-
try {
|
|
102
|
-
result = decodeURIComponent(result);
|
|
103
|
-
result = result.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => {
|
|
104
|
-
return String.fromCharCode(parseInt(hex, 16));
|
|
105
|
-
});
|
|
106
|
-
result = result.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => {
|
|
107
|
-
return String.fromCharCode(parseInt(hex, 16));
|
|
108
|
-
});
|
|
109
|
-
result = result.replace(/&#(\d+);/g, (_, num) => {
|
|
110
|
-
return String.fromCharCode(parseInt(num, 10));
|
|
111
|
-
});
|
|
112
|
-
result = result.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => {
|
|
113
|
-
return String.fromCharCode(parseInt(hex, 16));
|
|
114
|
-
});
|
|
115
|
-
} catch {
|
|
116
|
-
}
|
|
117
|
-
return result;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Enhanced polyglot attack handling - Addresses sophisticated XSS techniques
|
|
121
|
-
*/
|
|
122
|
-
static handlePolyglotAttacks(str) {
|
|
123
|
-
let result = str;
|
|
124
|
-
try {
|
|
125
|
-
result = result.replace(POLYGLOT_JAVASCRIPT, "");
|
|
126
|
-
result = result.replace(POLYGLOT_EVENTS, "");
|
|
127
|
-
result = result.replace(ENCODED_TAGS, "");
|
|
128
|
-
result = result.replace(/javascript\s*:\s*\/\*[\s\S]*?\*\//gi, "");
|
|
129
|
-
result = result.replace(/javascript\s*:\s*\/\*[^*]*\*\/[^>]*/gi, "");
|
|
130
|
-
result = result.replace(/<form[^>]*>[\s\S]*?<\/form>/gi, "");
|
|
131
|
-
result = result.replace(/<math[^>]*>[\s\S]*?<\/math>/gi, "");
|
|
132
|
-
result = result.replace(/%0[AD]/gi, "");
|
|
133
|
-
if (/svg|sVg|\x3c|\\\w{3}/i.test(result)) {
|
|
134
|
-
result = result.replace(/svg[^>]*>/gi, "");
|
|
135
|
-
result = result.replace(/sVg[^>]*>/gi, "");
|
|
136
|
-
result = result.replace(/\\x\w{2}/gi, "");
|
|
137
|
-
result = result.replace(/<\/[^>]*>/gi, "");
|
|
138
|
-
result = result.replace(/<[^>]*>/gi, "");
|
|
139
|
-
}
|
|
140
|
-
} catch {
|
|
141
|
-
result = result.replace(/<[^>]*>/g, "").replace(/[<>]/g, "");
|
|
142
|
-
}
|
|
143
|
-
return result;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Assess threat level of input content
|
|
147
|
-
*/
|
|
148
|
-
static assessThreatLevel(input) {
|
|
149
|
-
if (!input) return "none";
|
|
150
|
-
if (/<script/i.test(input) || /javascript\s*:/i.test(input) || /on\w+\s*=/i.test(input)) {
|
|
151
|
-
return "critical";
|
|
152
|
-
}
|
|
153
|
-
if (/<(iframe|object|embed|form|svg|math)/i.test(input) || /eval\s*\(/i.test(input)) {
|
|
154
|
-
return "high";
|
|
155
|
-
}
|
|
156
|
-
if (/<(style|link|meta)/i.test(input) || /expression\s*\(/i.test(input)) {
|
|
157
|
-
return "medium";
|
|
158
|
-
}
|
|
159
|
-
if (/<[^>]*>/i.test(input)) {
|
|
160
|
-
return "low";
|
|
161
|
-
}
|
|
162
|
-
return "none";
|
|
271
|
+
return analyze(input, options);
|
|
163
272
|
}
|
|
164
273
|
/**
|
|
165
274
|
* Check if input contains dangerous patterns
|
|
166
|
-
*
|
|
275
|
+
*
|
|
167
276
|
* @param input - Content to check
|
|
168
277
|
* @returns true if dangerous content detected
|
|
169
|
-
*
|
|
278
|
+
*
|
|
170
279
|
* @example
|
|
171
280
|
* ```typescript
|
|
172
281
|
* const isDangerous = Purifai.isDangerous('<script>alert("xss")</script>');
|
|
@@ -174,20 +283,15 @@ var Purifai = class {
|
|
|
174
283
|
* ```
|
|
175
284
|
*/
|
|
176
285
|
static isDangerous(input) {
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
return DANGEROUS_TAGS_WITH_CONTENT.test(input) || EVENT_HANDLERS_ENHANCED.test(input) || DANGEROUS_PROTOCOLS_ENHANCED.test(input) || DANGEROUS_ATTRIBUTES.test(input) || TEMPLATE_INJECTION.test(input) || POLYGLOT_JAVASCRIPT.test(input) || POLYGLOT_EVENTS.test(input);
|
|
180
|
-
} catch {
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
286
|
+
return isDangerous(input);
|
|
183
287
|
}
|
|
184
288
|
/**
|
|
185
289
|
* Batch sanitize multiple inputs for optimal performance
|
|
186
|
-
*
|
|
290
|
+
*
|
|
187
291
|
* @param inputs - Array of inputs to sanitize
|
|
188
292
|
* @param options - Optional configuration
|
|
189
293
|
* @returns Array of sanitized strings
|
|
190
|
-
*
|
|
294
|
+
*
|
|
191
295
|
* @example
|
|
192
296
|
* ```typescript
|
|
193
297
|
* const cleaned = Purifai.sanitizeBatch([
|
|
@@ -198,57 +302,73 @@ var Purifai = class {
|
|
|
198
302
|
* ```
|
|
199
303
|
*/
|
|
200
304
|
static sanitizeBatch(inputs, options) {
|
|
201
|
-
return inputs
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
305
|
+
return sanitizeBatch(inputs, options);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Escape text for an HTML body context (lossless — nothing is removed)
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```typescript
|
|
312
|
+
* Purifai.escape('if (a<b && c>d)'); // "if (a<b && c>d)"
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
static escape(input) {
|
|
316
|
+
return escape(input);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Escape text for an HTML attribute value, safe even when unquoted
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```typescript
|
|
323
|
+
* `<div title="${Purifai.escapeAttribute(userInput)}">`
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
static escapeAttribute(input) {
|
|
327
|
+
return escapeAttribute(input);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Escape a URL, returning '' if its protocol is not allowed
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```typescript
|
|
334
|
+
* Purifai.escapeUrl('javascript:alert(1)'); // ""
|
|
335
|
+
* Purifai.escapeUrl('https://example.com'); // escaped, safe for href
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
static escapeUrl(input, options) {
|
|
339
|
+
return escapeUrl(input, options);
|
|
208
340
|
}
|
|
209
341
|
/**
|
|
210
342
|
* Get version information
|
|
211
343
|
*/
|
|
212
344
|
static getVersion() {
|
|
213
|
-
return
|
|
345
|
+
return getVersion();
|
|
214
346
|
}
|
|
215
347
|
/**
|
|
216
348
|
* Get performance and security statistics
|
|
217
349
|
*/
|
|
218
350
|
static getStats() {
|
|
219
|
-
return
|
|
220
|
-
version: this.getVersion(),
|
|
221
|
-
securityLevel: "100% XSS Protection",
|
|
222
|
-
performance: "Optimized for high-throughput"
|
|
223
|
-
};
|
|
351
|
+
return getStats();
|
|
224
352
|
}
|
|
225
353
|
};
|
|
226
|
-
Purifai.defaultOptions = {
|
|
227
|
-
maxLength: 1e6,
|
|
228
|
-
// 1MB
|
|
229
|
-
allowBasicHtml: false,
|
|
230
|
-
allowedProtocols: ["http", "https", "mailto"],
|
|
231
|
-
aggressiveMode: true
|
|
232
|
-
};
|
|
233
|
-
var sanitize = Purifai.sanitize;
|
|
234
|
-
var analyze = Purifai.analyze;
|
|
235
|
-
var isDangerous = Purifai.isDangerous;
|
|
236
|
-
var sanitizeBatch = Purifai.sanitizeBatch;
|
|
237
354
|
var index_default = Purifai;
|
|
238
355
|
export {
|
|
239
356
|
Purifai,
|
|
240
357
|
analyze,
|
|
241
358
|
index_default as default,
|
|
359
|
+
escape,
|
|
360
|
+
escapeAttribute,
|
|
361
|
+
escapeUrl,
|
|
242
362
|
isDangerous,
|
|
243
363
|
sanitize,
|
|
244
364
|
sanitizeBatch
|
|
245
365
|
};
|
|
246
366
|
/**
|
|
247
367
|
* 🛡️ PURIFAI - Ultra-Secure HTML Sanitizer
|
|
248
|
-
*
|
|
368
|
+
*
|
|
249
369
|
* Advanced XSS protection with polyglot attack resistance.
|
|
250
370
|
* Blocks sophisticated obfuscation techniques that bypass other sanitizers.
|
|
251
|
-
*
|
|
252
|
-
* @version
|
|
371
|
+
*
|
|
372
|
+
* @version 2.0.0
|
|
253
373
|
* @license MIT
|
|
254
374
|
*/
|