mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-10-18 14:29:01 +08:00
a90dad1996
Closes #9434
178 lines
11 KiB
TypeScript
178 lines
11 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { evaluateScriptCallback, evaluateScriptData } from './evaluate-script';
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe('serialized script data', () => {
|
|
it.each(['__DATA__', 'window.__DATA__', 'globalThis.__DATA__', 'self.__DATA__'])('normalizes the global target %s', async (target) => {
|
|
await expect(evaluateScriptData(String.raw`window.__DATA__ = { title: "Hello\u0020世界", tags: ["a", "b"] };`, target)).resolves.toEqual({ title: 'Hello 世界', tags: ['a', 'b'] });
|
|
});
|
|
|
|
it('resolves variables, nested assignments, and data-only unary and logical expressions', async () => {
|
|
const source = 'var title="A & B", unused=analytics(); window.pixiv=window.pixiv||{}; pixiv.novel={title, count:-2, available:!0, absent:void 0};';
|
|
await expect(evaluateScriptData(source, 'window.pixiv.novel')).resolves.toEqual({ title: 'A & B', count: -2, available: true, absent: undefined });
|
|
});
|
|
|
|
it('reads parent-object assignments and static bracket targets', async () => {
|
|
await expect(evaluateScriptData('window.pixiv={novel:{title:"Book"}};', 'self["pixiv"].novel')).resolves.toEqual({ title: 'Book' });
|
|
});
|
|
|
|
it('reads Nuxt IIFE parameters and object mutations without changing outer bindings', async () => {
|
|
const source = 'var a="outer";window.__NUXT__=(function(a,b,c){a.title=b;a.tags=c;return {data:[a,a],error:null}}({},"News",["tag"]));';
|
|
const value = await evaluateScriptData<{ data: Array<{ title: string; tags: string[] }>; error: null }>(source, '__NUXT__');
|
|
expect(value).toEqual({
|
|
data: [
|
|
{ title: 'News', tags: ['tag'] },
|
|
{ title: 'News', tags: ['tag'] },
|
|
],
|
|
error: null,
|
|
});
|
|
expect(value.data[0]).toBe(value.data[1]);
|
|
await expect(evaluateScriptData(source, 'a')).resolves.toBe('outer');
|
|
});
|
|
|
|
it('preserves repeated var declarations and evaluates unused IIFE arguments in order', async () => {
|
|
const source = 'var a={value:1};var a;window.__DATA__=(function(x){return x})(a,a.value=2);';
|
|
await expect(evaluateScriptData(source, '__DATA__')).resolves.toEqual({ value: 2 });
|
|
});
|
|
|
|
it('reads bounded Nuxt sparse-array arguments without calling the host Array constructor', async () => {
|
|
const source = 'window.__NUXT__=(function(a){a[1]={title:"News"};return {data:a}})(Array(3));';
|
|
const value = await evaluateScriptData<{ data: unknown[] }>(source, '__NUXT__');
|
|
expect(value.data).toHaveLength(3);
|
|
expect(0 in value.data).toBe(false);
|
|
expect(value.data[1]).toEqual({ title: 'News' });
|
|
expect(2 in value.data).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
'window.__DATA__=Array(-1);',
|
|
'window.__DATA__=Array(1.5);',
|
|
'window.__DATA__=Array(10000);',
|
|
'window.__DATA__=Array("3");',
|
|
'window.__DATA__=Array(1,2);',
|
|
'var Array=undefined;window.__DATA__=Array(3);',
|
|
'window.__DATA__=(function(Array){return Array(3)})(undefined);',
|
|
'window.__DATA__=(function(){var a=Array(3);var Array;return a})();',
|
|
'window.__DATA__=Array(3);function Array(){return ["shadowed"]}',
|
|
'window.__DATA__=(function Array(){return Array(3)})();',
|
|
'window.__DATA__=(function(){return Array(3);function Array(){return ["shadowed"]}})();',
|
|
'window.__DATA__=Array(3);if(false){var Array;}',
|
|
'window.__DATA__=Array(3);class Array {}',
|
|
'window.__DATA__=Array(3);var {constructorAlias:Array}={};',
|
|
'window.__DATA__=Array(3);var [Array]=[];',
|
|
'window.__DATA__=Array(3);var {nested:{Array=undefined}}={};',
|
|
'window.__DATA__=Array(3);var {...Array}={};',
|
|
'window.__DATA__=Array(3);if(false){var [Array]=[];}',
|
|
'window.__DATA__=(function(){return Array(3);try{}catch(error){var Array}})();',
|
|
])('rejects invalid or shadowed array constructors: %s', async (source) => {
|
|
await expect(evaluateScriptData(source, '__DATA__')).rejects.toThrow(/bounded|Shadowed|Unsupported|simple variable names/);
|
|
});
|
|
|
|
it('does not hoist bindings from nested functions or unrelated lexical blocks', async () => {
|
|
const source = 'window.__DATA__=Array(3);function unrelated(){var Array;}if(false){let Array;class Other{}}';
|
|
const value = await evaluateScriptData<unknown[]>(source, '__DATA__');
|
|
expect(value).toHaveLength(3);
|
|
expect(0 in value).toBe(false);
|
|
});
|
|
|
|
it('enforces a total complexity budget across multiple sparse arrays', async () => {
|
|
await expect(evaluateScriptData('window.__DATA__=[Array(6000),Array(6000)];', '__DATA__')).rejects.toThrow('complexity limit');
|
|
});
|
|
|
|
it('does not call unrelated application code or access the host global object', async () => {
|
|
const fetchSpy = vi.fn();
|
|
vi.stubGlobal('fetch', fetchSpy);
|
|
|
|
const value = await evaluateScriptData('globalThis.__scriptDataSideEffect="changed";fetch("https://example.invalid/");window.__DATA__={ok:true};', '__DATA__');
|
|
|
|
expect(value).toEqual({ ok: true });
|
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
expect('__scriptDataSideEffect' in globalThis).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
'window.__DATA__ = fetch("https://example.invalid/");',
|
|
'window.__DATA__ = Function("return 42")();',
|
|
'window.__DATA__ = { get secret() { return fetch("https://example.invalid/"); } };',
|
|
'window.__DATA__ = (function(){ while(true){} return {}; })();',
|
|
'window.__DATA__ = (function(){ return {}; })(fetch("https://example.invalid/"));',
|
|
])('rejects unsupported expressions required by the target without executing them', async (source) => {
|
|
const fetchSpy = vi.fn();
|
|
vi.stubGlobal('fetch', fetchSpy);
|
|
await expect(evaluateScriptData(source, '__DATA__')).rejects.toThrow(/Unsupported|ordinary/);
|
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each(['__proto__', 'constructor', 'prototype'])('rejects the unsafe property %s', async (key) => {
|
|
await expect(evaluateScriptData(`window.__DATA__={"${key}":{polluted:true}};`, '__DATA__')).rejects.toThrow('Unsafe script data property');
|
|
await expect(evaluateScriptData(`var value={};value["${key}"]={polluted:true};window.__DATA__=value;`, '__DATA__')).rejects.toThrow('Unsafe script data property');
|
|
expect(Object.hasOwn(Object.prototype, 'polluted')).toBe(false);
|
|
});
|
|
|
|
it.each(['window.__DATA__()', 'window[computed]', 'window.__DATA__; unrelated()', 'window.__proto__'])('rejects unsupported target paths: %s', async (target) => {
|
|
await expect(evaluateScriptData('window.__DATA__={};', target)).rejects.toThrow();
|
|
});
|
|
|
|
it('reports missing targets and unresolved variable dependencies', async () => {
|
|
await expect(evaluateScriptData('unrelated();', '__DATA__')).rejects.toThrow('target was not found');
|
|
await expect(evaluateScriptData('var data=unrelated();window.__DATA__=data;', '__DATA__')).rejects.toThrow('Unsupported script data expression');
|
|
});
|
|
|
|
it.each(['if(true){window.__DATA__.value=2;}', 'Object.assign(window.__DATA__,{value:2});', 'window.__DATA__.value++;', 'window.__DATA__[unknown()]={};'])(
|
|
'rejects unsupported code that could change extracted data: %s',
|
|
async (suffix) => {
|
|
await expect(evaluateScriptData(`window.__DATA__={value:1};${suffix}`, '__DATA__')).rejects.toThrow('Unsupported script data');
|
|
}
|
|
);
|
|
|
|
it.each(['if(true){window.__DATA__.newField=2;}', 'var ignored=Object.assign(window.__DATA__,{value:2});', 'window.ignored=Object.assign(window.__DATA__,{value:2});'])(
|
|
'rejects mutations hidden in new properties and unused assignments: %s',
|
|
async (suffix) => {
|
|
await expect(evaluateScriptData(`window.__DATA__={};${suffix}`, '__DATA__')).rejects.toThrow('Unsupported script data');
|
|
}
|
|
);
|
|
|
|
it('rejects cyclic data and excessively large sources or sparse arrays', async () => {
|
|
await expect(evaluateScriptData('var a={};a.self=a;window.__DATA__=a;', '__DATA__')).rejects.toThrow('Cyclic script data');
|
|
await expect(evaluateScriptData(' '.repeat(2_000_001), '__DATA__')).rejects.toThrow('size limit');
|
|
await expect(evaluateScriptData('window.__DATA__=(function(){var a=[];a[100000]={};return a})();', '__DATA__')).rejects.toThrow('bounded numeric indexes');
|
|
});
|
|
});
|
|
|
|
describe('serialized callback data', () => {
|
|
it('extracts a DWR callback argument without invoking the callback', async () => {
|
|
const callback = vi.fn();
|
|
vi.stubGlobal('dwr', { engine: { _remoteHandleCallback: callback } });
|
|
const source = '(function(){if(!window.dwr)return;var s0=[],s1={};s1.post={title:"Entry"};s0[0]=s1;dwr.engine._remoteHandleCallback("493053","0",s0);})();';
|
|
|
|
await expect(evaluateScriptCallback(source, 'dwr.engine._remoteHandleCallback')).resolves.toEqual([{ post: { title: 'Entry' } }]);
|
|
expect(callback).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('supports empty public DWR responses and explicit argument positions', async () => {
|
|
const source = '//#DWR-INSERT\n//#DWR-REPLY\ndwr.engine._remoteHandleCallback("493053","0",[]);';
|
|
await expect(evaluateScriptCallback(source, 'window.dwr.engine._remoteHandleCallback')).resolves.toEqual([]);
|
|
await expect(evaluateScriptCallback(source, 'dwr.engine._remoteHandleCallback', 0)).resolves.toBe('493053');
|
|
});
|
|
|
|
it('evaluates all callback arguments in order and does not keep an earlier successful capture after an invalid one', async () => {
|
|
const source = 'var a={value:1};dwr.engine._remoteHandleCallback(a.value=2,"0",a);';
|
|
await expect(evaluateScriptCallback(source, 'dwr.engine._remoteHandleCallback')).resolves.toEqual({ value: 2 });
|
|
await expect(evaluateScriptCallback(`${source}dwr.engine._remoteHandleCallback("0","0",unknown());`, 'dwr.engine._remoteHandleCallback')).rejects.toThrow();
|
|
});
|
|
|
|
it('rejects missing callbacks and external calls used as the data argument', async () => {
|
|
const fetchSpy = vi.fn();
|
|
vi.stubGlobal('fetch', fetchSpy);
|
|
await expect(evaluateScriptCallback('unrelated();', 'dwr.engine._remoteHandleCallback')).rejects.toThrow('callback was not found');
|
|
await expect(evaluateScriptCallback('dwr.engine._remoteHandleCallback("0","0",fetch("https://example.invalid/"));', 'dwr.engine._remoteHandleCallback')).rejects.toThrow();
|
|
await expect(evaluateScriptCallback('', 'callback', -1)).rejects.toThrow('non-negative integer');
|
|
await expect(evaluateScriptCallback('dwr["engine._remoteHandleCallback"]("0","0",[]);', 'dwr.engine._remoteHandleCallback')).rejects.toThrow('callback was not found');
|
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|