feat: add reserved words loading and identifier obfuscation to AssetProcessor

This commit is contained in:
Florian Sylvain
2025-09-16 22:03:02 +02:00
parent 4bf902bb55
commit 91e13a5c83
2 changed files with 270 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
{
"javascript": [
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"export",
"extends",
"finally",
"for",
"function",
"if",
"import",
"in",
"instanceof",
"let",
"new",
"return",
"super",
"switch",
"this",
"throw",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield",
"true",
"false",
"null",
"undefined",
"NaN",
"Infinity",
"console",
"window",
"document",
"Array",
"Object",
"String",
"Number",
"Boolean",
"Date",
"Math",
"JSON",
"setTimeout",
"setInterval",
"clearTimeout",
"clearInterval",
"addEventListener",
"removeEventListener",
"querySelector",
"querySelectorAll",
"getElementById",
"getElementsByClassName",
"getElementsByTagName",
"createElement",
"appendChild",
"removeChild",
"innerHTML",
"innerText",
"textContent",
"value",
"length",
"push",
"pop",
"shift",
"unshift",
"slice",
"splice",
"indexOf",
"includes",
"forEach",
"map",
"filter",
"reduce",
"find",
"some",
"every",
"sort",
"reverse",
"join",
"split",
"matchMedia",
"matches",
"classList",
"add",
"remove",
"contains",
"toggle",
"setAttribute",
"getAttribute",
"removeAttribute",
"target",
"isIntersecting",
"IntersectionObserver",
"unobserve",
"observe",
"threshold",
"rootMargin",
"key",
"click",
"keydown",
"DOMContentLoaded",
"onload",
"Function",
"of"
]
}
+153
View File
@@ -11,6 +11,18 @@ class AssetProcessor {
constructor() { constructor() {
this.files = new Map(); this.files = new Map();
this.cspHashes = { scripts: new Set(), styles: new Set() }; this.cspHashes = { scripts: new Set(), styles: new Set() };
this.reservedWords = this.loadReservedWords();
}
loadReservedWords() {
try {
const reservedWordsPath = path.join(ROOT, "reserved-words.json");
const data = fs.readFileSync(reservedWordsPath, "utf8");
return new Set(JSON.parse(data).javascript);
} catch (error) {
console.warn("Could not load reserved words, using fallback");
return new Set(["console", "window", "document"]); // minimal fallback
}
} }
hash(content, algorithm = "sha256") { hash(content, algorithm = "sha256") {
@@ -165,6 +177,145 @@ class AssetProcessor {
.trim(); .trim();
} }
generateObfuscatedName(index) {
const chars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let result = "";
let num = index;
result += chars[num % 52];
num = Math.floor(num / 52);
while (num > 0) {
result += chars[num % 62];
num = Math.floor(num / 62);
}
return result;
}
obfuscateJS(src) {
let out = src;
const preserveStrings = [];
const preserveRegex = [];
const preserveTemplates = [];
let stringIndex = 0;
let regexIndex = 0;
let templateIndex = 0;
out = out.replace(/`((?:\\.|[^`\\])*)`/g, (match) => {
const placeholder = `__TEMPLATE_${templateIndex++}__`;
preserveTemplates.push({ placeholder, original: match });
return placeholder;
});
out = out.replace(/(["'])((?:\\.|(?!\1)[^\\])*?)\1/g, (match) => {
const placeholder = `__STRING_${stringIndex++}__`;
preserveStrings.push({ placeholder, original: match });
return placeholder;
});
out = out.replace(
/\/(?![\/\*])([^\/\\\n]|\\.)*\/[gimuy]*/g,
(match) => {
const placeholder = `__REGEX_${regexIndex++}__`;
preserveRegex.push({ placeholder, original: match });
return placeholder;
}
);
const identifiers = new Set();
const identifierRegex = /\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/g;
let match;
while ((match = identifierRegex.exec(out)) !== null) {
const identifier = match[0];
if (
!this.reservedWords.has(identifier) &&
!identifier.startsWith("__")
) {
identifiers.add(identifier);
}
}
const obfuscationMap = new Map();
let nameIndex = 0;
Array.from(identifiers)
.sort()
.forEach((identifier) => {
obfuscationMap.set(
identifier,
this.generateObfuscatedName(nameIndex++)
);
});
obfuscationMap.forEach((obfuscated, original) => {
const patterns = [
new RegExp(
`\\b(const|let|var)\\s+${original.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&"
)}\\b`,
"g"
),
new RegExp(
`\\bfunction\\s+${original.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&"
)}\\b`,
"g"
),
new RegExp(
`\\(([^)]*\\b)${original.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&"
)}\\b([^)]*)\\)`,
"g"
),
new RegExp(
`\\.${original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
"g"
),
new RegExp(
`(?<!for\\s*\\([^)]*\\s)\\b${original.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&"
)}(?!\\s+of\\b)\\b`,
"g"
),
];
patterns.forEach((pattern) => {
out = out.replace(pattern, (match) => {
return match.replace(
new RegExp(
`\\b${original.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&"
)}\\b`,
"g"
),
obfuscated
);
});
});
});
preserveTemplates.forEach(({ placeholder, original }) => {
out = out.replace(placeholder, original);
});
preserveRegex.forEach(({ placeholder, original }) => {
out = out.replace(placeholder, original);
});
preserveStrings.forEach(({ placeholder, original }) => {
out = out.replace(placeholder, original);
});
return out;
}
minifyJS(src) { minifyJS(src) {
let out = src; let out = src;
const preserveStrings = []; const preserveStrings = [];
@@ -188,6 +339,8 @@ class AssetProcessor {
out = out.replace(placeholder, original); out = out.replace(placeholder, original);
}); });
out = this.obfuscateJS(out);
return out; return out;
} }