diff --git a/rewriter/src/lib.rs b/rewriter/src/lib.rs index f67e0fa..b18385c 100644 --- a/rewriter/src/lib.rs +++ b/rewriter/src/lib.rs @@ -2,8 +2,8 @@ pub mod rewrite; use std::{panic, str::FromStr}; -use js_sys::Function; -use rewrite::{rewrite, EncodeFn}; +use js_sys::{Function, Object, Reflect}; +use rewrite::{rewrite, Config, EncodeFn}; use url::Url; use wasm_bindgen::prelude::*; @@ -30,42 +30,58 @@ fn create_encode_function(encode: Function) -> EncodeFn { } #[wasm_bindgen] -pub fn rewrite_js( - js: &str, - url: &str, - prefix: String, - encode: Function, - wrapfn: String, - importfn: String, -) -> Vec { +pub fn rewrite_js(js: &str, url: &str, config: Object) -> Vec { rewrite( js, Url::from_str(url).unwrap(), - prefix, - create_encode_function(encode), - wrapfn, - importfn, + Config { + prefix: Reflect::get(&config, &"prefix".into()) + .unwrap() + .as_string() + .unwrap(), + encode: create_encode_function(Reflect::get(&config, &"encode".into()).unwrap().into()), + wrapfn: Reflect::get(&config, &"wrapfn".into()) + .unwrap() + .as_string() + .unwrap(), + importfn: Reflect::get(&config, &"importfn".into()) + .unwrap() + .as_string() + .unwrap(), + rewritefn: Reflect::get(&config, &"rewritefn".into()) + .unwrap() + .as_string() + .unwrap(), + }, ) } #[wasm_bindgen] -pub fn rewrite_js_from_arraybuffer( - js: &[u8], - url: &str, - prefix: String, - encode: Function, - wrapfn: String, - importfn: String, -) -> Vec { +pub fn rewrite_js_from_arraybuffer(js: &[u8], url: &str, config: Object) -> Vec { // we know that this is a valid utf-8 string let js = unsafe { std::str::from_utf8_unchecked(js) }; rewrite( js, Url::from_str(url).unwrap(), - prefix, - create_encode_function(encode), - wrapfn, - importfn, + Config { + prefix: Reflect::get(&config, &"prefix".into()) + .unwrap() + .as_string() + .unwrap(), + encode: create_encode_function(Reflect::get(&config, &"encode".into()).unwrap().into()), + wrapfn: Reflect::get(&config, &"wrapfn".into()) + .unwrap() + .as_string() + .unwrap(), + importfn: Reflect::get(&config, &"importfn".into()) + .unwrap() + .as_string() + .unwrap(), + rewritefn: Reflect::get(&config, &"rewritefn".into()) + .unwrap() + .as_string() + .unwrap(), + }, ) } diff --git a/rewriter/src/main.rs b/rewriter/src/main.rs index 40a32fc..0f21413 100644 --- a/rewriter/src/main.rs +++ b/rewriter/src/main.rs @@ -11,6 +11,8 @@ pub mod rewrite; use rewrite::rewrite; use url::Url; +use crate::rewrite::Config; + // Instruction: // create a `test.js`, // run `cargo run -p oxc_parser --example visitor` @@ -112,10 +114,13 @@ fn main() -> std::io::Result<()> { rewrite( &source_text, Url::from_str("https://google.com/glorngle/si.js").unwrap(), - "/scrammedjet/".to_string(), - Box::new(encode_string), - "$wrap".to_string(), - "$import".to_string(), + Config { + prefix: "/scrammedjet/".to_string(), + encode: Box::new(encode_string), + wrapfn: "$wrap".to_string(), + importfn: "$import".to_string(), + rewritefn: "$rewrite".to_string(), + } ) .as_slice() ) diff --git a/rewriter/src/rewrite.rs b/rewriter/src/rewrite.rs index 1ab8eb1..00e4fea 100644 --- a/rewriter/src/rewrite.rs +++ b/rewriter/src/rewrite.rs @@ -30,18 +30,24 @@ pub type EncodeFn = Box String>; struct Rewriter { jschanges: Vec, base: Url, - prefix: String, - wrapfn: String, - importfn: String, - encode: EncodeFn, + config: Config, } + +pub struct Config { + pub prefix: String, + pub wrapfn: String, + pub importfn: String, + pub rewritefn: String, + pub encode: EncodeFn, +} + impl Rewriter { fn rewrite_url(&mut self, url: String) -> String { let url = self.base.join(&url).unwrap(); - let urlencoded = (self.encode)(url.to_string()); + let urlencoded = (self.config.encode)(url.to_string()); - format!("\"{}{}\"", self.prefix, urlencoded) + format!("\"{}{}\"", self.config.prefix, urlencoded) } } @@ -57,24 +63,49 @@ impl<'a> Visit<'a> for Rewriter { if UNSAFE_GLOBALS.contains(&it.name.to_string().as_str()) { self.jschanges.push(JsChange::GenericChange { span: it.span, - text: format!("({}({}))", self.wrapfn, it.name), + text: format!("({}({}))", self.config.wrapfn, it.name), }); } } fn visit_this_expression(&mut self, it: &oxc_ast::ast::ThisExpression) { self.jschanges.push(JsChange::GenericChange { span: it.span, - text: format!("({}(this))", self.wrapfn), + text: format!("({}(this))", self.config.wrapfn), }); } fn visit_debugger_statement(&mut self, it: &oxc_ast::ast::DebuggerStatement) { + // delete debugger statements entirely. some sites will spam debugger as an anti-debugging measure, and we don't want that! self.jschanges.push(JsChange::GenericChange { span: it.span, text: "".to_string(), }); } + // we can't overwrite window.eval in the normal way because that would make everything an + // indirect eval, which could break things. we handle that edge case here + fn visit_call_expression(&mut self, it: &oxc_ast::ast::CallExpression<'a>) { + if let Expression::Identifier(s) = &it.callee { + // if it's optional that actually makes it an indirect eval which is handled separately + if s.name == "eval" && !it.optional { + self.jschanges.push(JsChange::GenericChange { + span: Span::new(s.span.start, s.span.end + 1), + text: format!("eval({}(", self.config.rewritefn), + }); + self.jschanges.push(JsChange::GenericChange { + span: Span::new(it.span.end, it.span.end), + text: ")".to_string(), + }); + + // then we walk the arguments, but not the callee, since we want it to resolve to + // the real eval + walk::walk_arguments(self, &it.arguments); + return; + } + } + walk::walk_call_expression(self, it); + } + fn visit_import_declaration(&mut self, it: &oxc_ast::ast::ImportDeclaration<'a>) { let name = it.source.value.to_string(); let text = self.rewrite_url(name); @@ -87,7 +118,7 @@ impl<'a> Visit<'a> for Rewriter { fn visit_import_expression(&mut self, it: &oxc_ast::ast::ImportExpression<'a>) { self.jschanges.push(JsChange::GenericChange { span: Span::new(it.span.start, it.span.start + 6), - text: format!("({}(\"{}\"))", self.importfn, self.base), + text: format!("({}(\"{}\"))", self.config.importfn, self.base), }); walk::walk_import_expression(self, it); } @@ -122,7 +153,7 @@ impl<'a> Visit<'a> for Rewriter { if UNSAFE_GLOBALS.contains(&s.name.to_string().as_str()) && p.shorthand { self.jschanges.push(JsChange::GenericChange { span: s.span, - text: format!("{}: ({}({}))", s.name, self.wrapfn, s.name), + text: format!("{}: ({}({}))", s.name, self.config.wrapfn, s.name), }); return; } @@ -238,7 +269,7 @@ fn expression_span(e: &Expression) -> Span { } // js MUST not be able to get a reference to any of these because sbx -const UNSAFE_GLOBALS: [&str; 8] = [ +const UNSAFE_GLOBALS: [&str; 9] = [ "window", "self", "globalThis", @@ -247,16 +278,10 @@ const UNSAFE_GLOBALS: [&str; 8] = [ "top", "location", "document", + "eval", ]; -pub fn rewrite( - js: &str, - url: Url, - prefix: String, - encode: EncodeFn, - wrapfn: String, - importfn: String, -) -> Vec { +pub fn rewrite(js: &str, url: Url, config: Config) -> Vec { let allocator = Allocator::default(); let source_type = SourceType::default(); let ret = Parser::new(&allocator, js, source_type).parse(); @@ -274,10 +299,7 @@ pub fn rewrite( let mut ast_pass = Rewriter { jschanges: Vec::new(), base: url, - prefix, - encode, - wrapfn, - importfn, + config, }; ast_pass.visit_program(&program); diff --git a/src/client/index.ts b/src/client/index.ts index 5ec0071..7407c13 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -8,10 +8,6 @@ export const issw = "ServiceWorkerGlobalScope" in self; export const isdedicated = "DedicatedWorkerGlobalScope" in self; export const isshared = "SharedWorkerGlobalScope" in self; -export const wrapfn = "$scramjet$wrap"; -export const trysetfn = "$scramjet$tryset"; -export const importfn = "$scramjet$import"; - dbg.log("scrammin"); // if it already exists, that means the handlers have probably already been setup by the parent document if (!(ScramjetClient.SCRAMJET in self)) { diff --git a/src/client/shared.ts b/src/client/shared.ts index c1ee55b..1115c30 100644 --- a/src/client/shared.ts +++ b/src/client/shared.ts @@ -10,3 +10,5 @@ export const { rewriteWorkers, }, } = self.$scramjet.shared; + +export const config = self.$scramjet.config; diff --git a/src/client/shared/eval.ts b/src/client/shared/eval.ts index 553e5e8..0c6a4bf 100644 --- a/src/client/shared/eval.ts +++ b/src/client/shared/eval.ts @@ -1,24 +1,47 @@ import { ScramjetClient, ProxyCtx } from "../client"; -import { rewriteJs } from "../shared"; +import { config, rewriteJs } from "../shared"; function rewriteFunction(ctx: ProxyCtx) { - for (const i in ctx.args) { - ctx.args[i] = rewriteJs(ctx.args[i]); - } + for (const i in ctx.args) { + ctx.args[i] = rewriteJs(ctx.args[i]); + } - ctx.return(ctx.fn(...ctx.args)); + ctx.return(ctx.fn(...ctx.args)); } export default function (client: ScramjetClient, self: Self) { - client.Proxy("Function", { - apply(ctx) { - rewriteFunction(ctx); - }, + client.Proxy("Function", { + apply(ctx) { + rewriteFunction(ctx); + }, - construct(ctx) { - rewriteFunction(ctx); - }, - }); + construct(ctx) { + rewriteFunction(ctx); + }, + }); - Function.prototype.constructor = Function; -} \ No newline at end of file + Function.prototype.constructor = Function; + + // used for proxying *direct eval* + // eval("...") -> eval($scramjet$rewrite("...")) + Object.defineProperty(self, config.rewritefn, { + value: function (js: any) { + if (typeof js !== "string") return js; + + const rewritten = rewriteJs(js, client.url); + + return rewritten; + }, + writable: false, + configurable: false, + }); +} + +export function indirectEval(this: ScramjetClient, js: any) { + // > If the argument of eval() is not a string, eval() returns the argument unchanged + if (typeof js !== "string") return js; + + const indirection = this.global.eval; + + return indirection(rewriteJs(js, this.url) as string); +} diff --git a/src/client/shared/import.ts b/src/client/shared/import.ts index 543f300..bc24862 100644 --- a/src/client/shared/import.ts +++ b/src/client/shared/import.ts @@ -1,8 +1,7 @@ -import { encodeUrl } from "../shared"; -import { importfn } from "../"; +import { config, encodeUrl } from "../shared"; export default function (client, self) { - self[importfn] = function (base) { + self[config.importfn] = function (base) { return function (url) { const resolved = new URL(url, base).href; diff --git a/src/client/shared/wrap.ts b/src/client/shared/wrap.ts index 3a4b87f..763f7ac 100644 --- a/src/client/shared/wrap.ts +++ b/src/client/shared/wrap.ts @@ -1,11 +1,12 @@ -import { iswindow, isworker, trysetfn, wrapfn } from ".."; +import { iswindow, isworker } from ".."; import { ScramjetClient } from "../client"; +import { config } from "../shared"; export default function (client: ScramjetClient, self: typeof globalThis) { // the main magic of the proxy. all attempts to access any "banned objects" will be redirected here, and instead served a proxy object // this contrasts from how other proxies will leave the root object alone and instead attempt to catch every member access // this presents some issues (see element.ts), but makes us a good bit faster at runtime! - Object.defineProperty(self, wrapfn, { + Object.defineProperty(self, config.wrapfn, { value: function (identifier: any, args: any) { if (args && typeof args === "object" && args.length === 0) for (const arg of args) { @@ -58,7 +59,7 @@ export default function (client: ScramjetClient, self: typeof globalThis) { // ((t)=>$scramjet$tryset(location,"+=",t)||location+=t)(...); // it has to be a discrete function because there's always the possibility that "location" is a local variable // we have to use an IIFE to avoid duplicating side-effects in the getter - Object.defineProperty(self, trysetfn, { + Object.defineProperty(self, config.trysetfn, { value: function (lhs: any, op: string, rhs: any) { if (lhs instanceof Location) { // @ts-ignore diff --git a/src/client/window.ts b/src/client/window.ts index ec90163..e5b9762 100644 --- a/src/client/window.ts +++ b/src/client/window.ts @@ -1,6 +1,7 @@ import { encodeUrl } from "../shared/rewriters/url"; import { ScramjetClient } from "./client"; -import { wrapfn } from "."; +import { indirectEval } from "./shared/eval"; +import { config } from "./shared"; export function createWindowProxy( client: ScramjetClient, @@ -9,16 +10,17 @@ export function createWindowProxy( return new Proxy(self, { get(target, prop) { const propIsString = typeof prop === "string"; - if (propIsString && prop === "location") { - return client.locationProxy; - } else if ( + if (prop === "location") return client.locationProxy; + + if ( propIsString && ["window", "top", "self", "globalThis", "parent"].includes(prop) - ) { - return self[wrapfn](self[prop]); - } else if (propIsString && prop === "$scramjet") { - return; - } + ) + return self[config.wrapfn](self[prop]); + + if (prop === "$scramjet") return; + + if (prop === "eval") return indirectEval.bind(client); const value = Reflect.get(target, prop); diff --git a/src/scramjet.config.ts b/src/scramjet.config.ts index 736622b..4e47107 100644 --- a/src/scramjet.config.ts +++ b/src/scramjet.config.ts @@ -5,6 +5,10 @@ if (!self.$scramjet) { self.$scramjet.config = { prefix: "/scramjet/", codec: self.$scramjet.codecs.plain, + wrapfn: "$scramjet$wrap", + trysetfn: "$scramjet$tryset", + importfn: "$scramjet$import", + rewritefn: "$scramjet$rewrite", config: "/scram/scramjet.config.js", shared: "/scram/scramjet.shared.js", worker: "/scram/scramjet.worker.js", diff --git a/src/shared/rewriters/js.ts b/src/shared/rewriters/js.ts index 4db866e..25064d2 100644 --- a/src/shared/rewriters/js.ts +++ b/src/shared/rewriters/js.ts @@ -24,25 +24,21 @@ export function rewriteJs(js: string | ArrayBuffer, origin?: URL) { if ("window" in globalThis) origin ??= new URL(decodeUrl(location.href)); const before = performance.now(); + const cfg = { + prefix: self.$scramjet.config.prefix, + codec: self.$scramjet.config.codec.encode, + wrapfn: self.$scramjet.config.wrapfn, + trysetfn: self.$scramjet.config.trysetfn, + importfn: self.$scramjet.config.importfn, + rewritefn: self.$scramjet.config.rewritefn, + }; if (typeof js === "string") { - js = new TextDecoder().decode( - rewrite_js( - js, - origin.toString(), - self.$scramjet.config.prefix, - self.$scramjet.config.codec.encode as any, - "$scramjet$wrap", - "$scramjet$import" - ) - ); + js = new TextDecoder().decode(rewrite_js(js, origin.toString(), cfg)); } else { js = rewrite_js_from_arraybuffer( new Uint8Array(js), origin.toString(), - self.$scramjet.config.prefix, - self.$scramjet.config.codec.encode as any, - "$scramjet$wrap", - "$scramjet$import" + cfg ); } const after = performance.now(); diff --git a/src/types.d.ts b/src/types.d.ts index 8a16769..d6f6f57 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -34,6 +34,10 @@ declare global { config: { prefix: string; codec: Codec; + wrapfn: string; + trysetfn: string; + importfn: string; + rewritefn: string; config: string; shared: string; worker: string;