UNPKG

rsshub

Version:
275 lines (274 loc) • 13.1 kB
import { parse } from "acorn"; //#region lib/utils/parse-script-data.ts const globalNames = /* @__PURE__ */ new Set([ "window", "globalThis", "self" ]); const unsafeKeys = /* @__PURE__ */ new Set([ "__proto__", "prototype", "constructor" ]); const maxScriptLength = 2e6; const maxSteps = 1e5; const maxDepth = 100; var ScriptDataError = class extends Error {}; var UnsafePropertyError = class extends ScriptDataError {}; const propertyKey = (value) => { if (typeof value !== "string" && typeof value !== "number") throw new ScriptDataError("Script data property keys must be strings or numbers"); const key = String(value); if (unsafeKeys.has(key)) throw new UnsafePropertyError(`Unsafe script data property: ${key}`); return key; }; const staticPath = (node) => { if (node.type === "Identifier") return [propertyKey(node.name)]; if (node.type === "MemberExpression" && !node.optional) { const key = !node.computed && node.property.type === "Identifier" ? node.property.name : node.property.type === "Literal" ? node.property.value : void 0; return [...staticPath(node.object), propertyKey(key)]; } throw new ScriptDataError("Script data targets must be static property paths"); }; const normalizePath = (path) => globalNames.has(path[0]) ? path.slice(1) : path; const targetPath = (target) => { const program = parse(target, { ecmaVersion: "latest" }); if (program.body.length !== 1 || program.body[0].type !== "ExpressionStatement") throw new ScriptDataError("Script data targets must be a single static property path"); const path = normalizePath(staticPath(program.body[0].expression)); if (!path.length) throw new ScriptDataError("Script data targets must name a property"); return path; }; const isObject = (value) => typeof value === "object" && value !== null && !(value instanceof ScriptDataError); const readProperty = (object, key) => { if (object instanceof ScriptDataError) throw object; if (!isObject(object)) throw new ScriptDataError("Cannot read a property of non-object script data"); return Object.hasOwn(object, key) ? object[key] : void 0; }; /** Reads serialized data with a limited AST vocabulary; it never executes JavaScript. */ var ScriptDataReader = class { target; argumentIndex; root = { values: Object.create(null) }; steps = 0; poisonedAssignments = /* @__PURE__ */ new WeakSet(); captured = false; callbackValue; constructor(target, argumentIndex) { this.target = target; this.argumentIndex = argumentIndex; } step(depth) { if (++this.steps > maxSteps || depth > maxDepth) throw new UnsafePropertyError("Script data exceeds the parsing complexity limit"); } identifier(name, scope) { propertyKey(name); for (let current = scope; current; current = current.parent) if (Object.hasOwn(current.values, name)) { const value = current.values[name]; if (value instanceof ScriptDataError) throw value; return value; } if (globalNames.has(name)) return this.root.values; if (name === "undefined") return; throw new ScriptDataError(`Unknown script data variable: ${name}`); } reference(node, scope, depth) { if (node.type === "Identifier") { const key = propertyKey(node.name); if (globalNames.has(key)) throw new ScriptDataError("Cannot replace script data global aliases"); let current = scope; while (current.parent && !Object.hasOwn(current.values, key)) current = current.parent; return { object: current.values, key }; } if (node.type !== "MemberExpression" || node.optional) throw new ScriptDataError("Unsupported script data assignment target"); const object = this.evaluate(node.object, scope, depth + 1); const key = propertyKey(!node.computed && node.property.type === "Identifier" ? node.property.name : this.evaluate(node.property, scope, depth + 1)); if (!isObject(object)) throw new ScriptDataError("Cannot assign a property of non-object script data"); if (Array.isArray(object) && (!/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= maxSteps)) throw new ScriptDataError("Script data arrays require bounded numeric indexes"); return { object, key }; } evaluate(node, scope, depth) { this.step(depth); switch (node.type) { case "Literal": if (node.regex || node.bigint) break; return node.value; case "Identifier": return this.identifier(node.name, scope); case "ArrayExpression": return node.elements.map((element) => element ? this.evaluate(element, scope, depth + 1) : void 0); case "ObjectExpression": { const value = Object.create(null); for (const property of node.properties) { if (property.type !== "Property" || property.kind !== "init" || property.method) throw new ScriptDataError("Only ordinary script data object properties are supported"); const key = propertyKey(!property.computed && property.key.type === "Identifier" ? property.key.name : this.evaluate(property.key, scope, depth + 1)); value[key] = this.evaluate(property.value, scope, depth + 1); } return value; } case "MemberExpression": { const key = propertyKey(!node.computed && node.property.type === "Identifier" ? node.property.name : this.evaluate(node.property, scope, depth + 1)); return readProperty(this.evaluate(node.object, scope, depth + 1), key); } case "UnaryExpression": { const value = this.evaluate(node.argument, scope, depth + 1); if (node.operator === "void") return; if (node.operator === "!") return !value; if (typeof value === "number" && (node.operator === "-" || node.operator === "+")) return node.operator === "-" ? -value : value; break; } case "LogicalExpression": { const left = this.evaluate(node.left, scope, depth + 1); if (node.operator === "||" && left || node.operator === "&&" && !left || node.operator === "??" && left !== void 0 && left !== null) return left; return this.evaluate(node.right, scope, depth + 1); } case "AssignmentExpression": { const { object, key } = this.reference(node.left, scope, depth + 1); try { if (node.operator !== "=") throw new ScriptDataError("Only simple script data assignments are supported"); const value = this.evaluate(node.right, scope, depth + 1); object[key] = value; return value; } catch (error) { if (error instanceof ScriptDataError) { object[key] = error; this.poisonedAssignments.add(node); } throw error; } } case "SequenceExpression": { let value; for (const expression of node.expressions) value = this.evaluate(expression, scope, depth + 1); return value; } case "CallExpression": { if (this.argumentIndex !== void 0 && this.matchesCallback(node.callee)) { this.captured = false; this.callbackValue = void 0; const args = node.arguments.map((argument) => this.evaluate(argument, scope, depth + 1)); if (this.argumentIndex >= args.length) throw new ScriptDataError("Script data callback is missing its data argument"); this.callbackValue = args[this.argumentIndex]; this.captured = true; return; } const fn = node.callee; if (fn.type !== "FunctionExpression" && fn.type !== "ArrowFunctionExpression" || fn.async || fn.generator) break; const args = node.arguments.map((argument) => this.evaluate(argument, scope, depth + 1)); const local = { values: Object.create(null), parent: scope }; for (const [index, parameter] of fn.params.entries()) { if (parameter.type !== "Identifier" || globalNames.has(parameter.name)) throw new ScriptDataError("Script data IIFEs require simple parameters"); const key = propertyKey(parameter.name); local.values[key] = args[index]; } return fn.body.type === "BlockStatement" ? this.statements(fn.body.body, local, depth + 1, true)?.value : this.evaluate(fn.body, local, depth + 1); } } throw new ScriptDataError(`Unsupported script data expression: ${node.type}`); } matchesCallback(node) { try { const path = normalizePath(staticPath(node)); return path.length === this.target.length && path.every((key, index) => key === this.target[index]); } catch (error) { if (error instanceof UnsafePropertyError) throw error; return false; } } isCallbackGuard(node) { if (this.argumentIndex === void 0 || node.type !== "IfStatement" || node.alternate || node.test.type !== "UnaryExpression" || node.test.operator !== "!") return false; const consequent = node.consequent.type === "BlockStatement" && node.consequent.body.length === 1 ? node.consequent.body[0] : node.consequent; if (consequent.type !== "ReturnStatement" || consequent.argument) return false; const path = normalizePath(staticPath(node.test.argument)); return path.length > 0 && path.length < this.target.length && path.every((key, index) => key === this.target[index]); } referencesKnownData(node, scope, depth) { this.step(depth); if (node.type === "Identifier" || node.type === "MemberExpression") try { const path = staticPath(node); let value = this.identifier(path[0], scope); for (const key of path.slice(1)) { if (!isObject(value) || !Object.hasOwn(value, key)) return value !== this.root.values; value = readProperty(value, key); } return true; } catch (error) { if (error instanceof UnsafePropertyError) throw error; if (node.type === "Identifier") return false; } return Object.values(node).some((value) => { return (Array.isArray(value) ? value : [value]).some((child) => child && typeof child === "object" && typeof child.type === "string" && this.referencesKnownData(child, scope, depth + 1)); }); } statements(nodes, scope, depth, strict) { for (const node of nodes) { this.step(depth); try { switch (node.type) { case "VariableDeclaration": for (const declaration of node.declarations) { if (declaration.id.type !== "Identifier" || globalNames.has(declaration.id.name)) throw new ScriptDataError("Script data declarations require simple variable names"); const key = propertyKey(declaration.id.name); try { if (declaration.init || !Object.hasOwn(scope.values, key)) scope.values[key] = declaration.init ? this.evaluate(declaration.init, scope, depth + 1) : void 0; } catch (error) { if (strict || !(error instanceof ScriptDataError) || error instanceof UnsafePropertyError || declaration.init && this.referencesKnownData(declaration.init, scope, depth + 1)) throw error; scope.values[key] = error; } } break; case "ExpressionStatement": this.evaluate(node.expression, scope, depth + 1); break; case "ReturnStatement": return { value: node.argument ? this.evaluate(node.argument, scope, depth + 1) : void 0 }; case "EmptyStatement": break; default: if (!this.isCallbackGuard(node)) throw new ScriptDataError(`Unsupported script data statement: ${node.type}`); } } catch (error) { if (strict || !(error instanceof ScriptDataError) || error instanceof UnsafePropertyError) throw error; if (!(node.type === "ExpressionStatement" && node.expression.type === "AssignmentExpression" && this.poisonedAssignments.has(node.expression) && !this.referencesKnownData(node.expression.right, scope, depth + 1)) && this.referencesKnownData(node, scope, depth + 1)) throw error; } } } validate(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) { this.step(depth); if (value instanceof ScriptDataError) throw value; if (isObject(value)) { if (ancestors.has(value)) throw new ScriptDataError("Cyclic script data is not supported"); ancestors.add(value); for (const child of Object.values(value)) this.validate(child, ancestors, depth + 1); ancestors.delete(value); } } read(source) { if (source.length > maxScriptLength) throw new ScriptDataError("Script data source exceeds the size limit"); this.statements(parse(source, { ecmaVersion: "latest" }).body, this.root, 0, false); let value = this.root.values; if (this.argumentIndex === void 0) for (const key of this.target) { if (!isObject(value) || !Object.hasOwn(value, key)) { if (value instanceof ScriptDataError) throw value; throw new ScriptDataError("Script data target was not found"); } value = readProperty(value, key); } else { if (!this.captured) throw new ScriptDataError("Script data callback was not found or could not be parsed"); value = this.callbackValue; } this.validate(value); return value; } }; const parseScriptData = (source, target) => new ScriptDataReader(targetPath(target)).read(source); /** Extracts a serialized callback argument without invoking the callback or any external function. */ const parseScriptCallback = (source, callbackPath, argumentIndex = 2) => { if (!Number.isSafeInteger(argumentIndex) || argumentIndex < 0) throw new ScriptDataError("Script data callback argument index must be a non-negative integer"); return new ScriptDataReader(targetPath(callbackPath), argumentIndex).read(source); }; //#endregion export { parseScriptData as n, parseScriptCallback as t };