implement direct and indirect eval

This commit is contained in:
velzie 2024-07-28 21:45:41 -04:00
parent e6b237c525
commit ec8421be8f
No known key found for this signature in database
GPG key ID: 048413F95F0DDE1F
12 changed files with 171 additions and 101 deletions

View file

@ -2,8 +2,8 @@ pub mod rewrite;
use std::{panic, str::FromStr}; use std::{panic, str::FromStr};
use js_sys::Function; use js_sys::{Function, Object, Reflect};
use rewrite::{rewrite, EncodeFn}; use rewrite::{rewrite, Config, EncodeFn};
use url::Url; use url::Url;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@ -30,42 +30,58 @@ fn create_encode_function(encode: Function) -> EncodeFn {
} }
#[wasm_bindgen] #[wasm_bindgen]
pub fn rewrite_js( pub fn rewrite_js(js: &str, url: &str, config: Object) -> Vec<u8> {
js: &str,
url: &str,
prefix: String,
encode: Function,
wrapfn: String,
importfn: String,
) -> Vec<u8> {
rewrite( rewrite(
js, js,
Url::from_str(url).unwrap(), Url::from_str(url).unwrap(),
prefix, Config {
create_encode_function(encode), prefix: Reflect::get(&config, &"prefix".into())
wrapfn, .unwrap()
importfn, .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] #[wasm_bindgen]
pub fn rewrite_js_from_arraybuffer( pub fn rewrite_js_from_arraybuffer(js: &[u8], url: &str, config: Object) -> Vec<u8> {
js: &[u8],
url: &str,
prefix: String,
encode: Function,
wrapfn: String,
importfn: String,
) -> Vec<u8> {
// we know that this is a valid utf-8 string // we know that this is a valid utf-8 string
let js = unsafe { std::str::from_utf8_unchecked(js) }; let js = unsafe { std::str::from_utf8_unchecked(js) };
rewrite( rewrite(
js, js,
Url::from_str(url).unwrap(), Url::from_str(url).unwrap(),
prefix, Config {
create_encode_function(encode), prefix: Reflect::get(&config, &"prefix".into())
wrapfn, .unwrap()
importfn, .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(),
},
) )
} }

View file

@ -11,6 +11,8 @@ pub mod rewrite;
use rewrite::rewrite; use rewrite::rewrite;
use url::Url; use url::Url;
use crate::rewrite::Config;
// Instruction: // Instruction:
// create a `test.js`, // create a `test.js`,
// run `cargo run -p oxc_parser --example visitor` // run `cargo run -p oxc_parser --example visitor`
@ -112,10 +114,13 @@ fn main() -> std::io::Result<()> {
rewrite( rewrite(
&source_text, &source_text,
Url::from_str("https://google.com/glorngle/si.js").unwrap(), Url::from_str("https://google.com/glorngle/si.js").unwrap(),
"/scrammedjet/".to_string(), Config {
Box::new(encode_string), prefix: "/scrammedjet/".to_string(),
"$wrap".to_string(), encode: Box::new(encode_string),
"$import".to_string(), wrapfn: "$wrap".to_string(),
importfn: "$import".to_string(),
rewritefn: "$rewrite".to_string(),
}
) )
.as_slice() .as_slice()
) )

View file

@ -30,18 +30,24 @@ pub type EncodeFn = Box<dyn Fn(String) -> String>;
struct Rewriter { struct Rewriter {
jschanges: Vec<JsChange>, jschanges: Vec<JsChange>,
base: Url, base: Url,
prefix: String, config: Config,
wrapfn: String,
importfn: String,
encode: EncodeFn,
} }
pub struct Config {
pub prefix: String,
pub wrapfn: String,
pub importfn: String,
pub rewritefn: String,
pub encode: EncodeFn,
}
impl Rewriter { impl Rewriter {
fn rewrite_url(&mut self, url: String) -> String { fn rewrite_url(&mut self, url: String) -> String {
let url = self.base.join(&url).unwrap(); 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()) { if UNSAFE_GLOBALS.contains(&it.name.to_string().as_str()) {
self.jschanges.push(JsChange::GenericChange { self.jschanges.push(JsChange::GenericChange {
span: it.span, 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) { fn visit_this_expression(&mut self, it: &oxc_ast::ast::ThisExpression) {
self.jschanges.push(JsChange::GenericChange { self.jschanges.push(JsChange::GenericChange {
span: it.span, 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) { 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 { self.jschanges.push(JsChange::GenericChange {
span: it.span, span: it.span,
text: "".to_string(), 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>) { fn visit_import_declaration(&mut self, it: &oxc_ast::ast::ImportDeclaration<'a>) {
let name = it.source.value.to_string(); let name = it.source.value.to_string();
let text = self.rewrite_url(name); 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>) { fn visit_import_expression(&mut self, it: &oxc_ast::ast::ImportExpression<'a>) {
self.jschanges.push(JsChange::GenericChange { self.jschanges.push(JsChange::GenericChange {
span: Span::new(it.span.start, it.span.start + 6), 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); 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 { if UNSAFE_GLOBALS.contains(&s.name.to_string().as_str()) && p.shorthand {
self.jschanges.push(JsChange::GenericChange { self.jschanges.push(JsChange::GenericChange {
span: s.span, span: s.span,
text: format!("{}: ({}({}))", s.name, self.wrapfn, s.name), text: format!("{}: ({}({}))", s.name, self.config.wrapfn, s.name),
}); });
return; 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 // 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", "window",
"self", "self",
"globalThis", "globalThis",
@ -247,16 +278,10 @@ const UNSAFE_GLOBALS: [&str; 8] = [
"top", "top",
"location", "location",
"document", "document",
"eval",
]; ];
pub fn rewrite( pub fn rewrite(js: &str, url: Url, config: Config) -> Vec<u8> {
js: &str,
url: Url,
prefix: String,
encode: EncodeFn,
wrapfn: String,
importfn: String,
) -> Vec<u8> {
let allocator = Allocator::default(); let allocator = Allocator::default();
let source_type = SourceType::default(); let source_type = SourceType::default();
let ret = Parser::new(&allocator, js, source_type).parse(); let ret = Parser::new(&allocator, js, source_type).parse();
@ -274,10 +299,7 @@ pub fn rewrite(
let mut ast_pass = Rewriter { let mut ast_pass = Rewriter {
jschanges: Vec::new(), jschanges: Vec::new(),
base: url, base: url,
prefix, config,
encode,
wrapfn,
importfn,
}; };
ast_pass.visit_program(&program); ast_pass.visit_program(&program);

View file

@ -8,10 +8,6 @@ export const issw = "ServiceWorkerGlobalScope" in self;
export const isdedicated = "DedicatedWorkerGlobalScope" in self; export const isdedicated = "DedicatedWorkerGlobalScope" in self;
export const isshared = "SharedWorkerGlobalScope" 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"); dbg.log("scrammin");
// if it already exists, that means the handlers have probably already been setup by the parent document // if it already exists, that means the handlers have probably already been setup by the parent document
if (!(ScramjetClient.SCRAMJET in self)) { if (!(ScramjetClient.SCRAMJET in self)) {

View file

@ -10,3 +10,5 @@ export const {
rewriteWorkers, rewriteWorkers,
}, },
} = self.$scramjet.shared; } = self.$scramjet.shared;
export const config = self.$scramjet.config;

View file

@ -1,24 +1,47 @@
import { ScramjetClient, ProxyCtx } from "../client"; import { ScramjetClient, ProxyCtx } from "../client";
import { rewriteJs } from "../shared"; import { config, rewriteJs } from "../shared";
function rewriteFunction(ctx: ProxyCtx) { function rewriteFunction(ctx: ProxyCtx) {
for (const i in ctx.args) { for (const i in ctx.args) {
ctx.args[i] = rewriteJs(ctx.args[i]); 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) { export default function (client: ScramjetClient, self: Self) {
client.Proxy("Function", { client.Proxy("Function", {
apply(ctx) { apply(ctx) {
rewriteFunction(ctx); rewriteFunction(ctx);
}, },
construct(ctx) { construct(ctx) {
rewriteFunction(ctx); rewriteFunction(ctx);
}, },
}); });
Function.prototype.constructor = Function; 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);
} }

View file

@ -1,8 +1,7 @@
import { encodeUrl } from "../shared"; import { config, encodeUrl } from "../shared";
import { importfn } from "../";
export default function (client, self) { export default function (client, self) {
self[importfn] = function (base) { self[config.importfn] = function (base) {
return function (url) { return function (url) {
const resolved = new URL(url, base).href; const resolved = new URL(url, base).href;

View file

@ -1,11 +1,12 @@
import { iswindow, isworker, trysetfn, wrapfn } from ".."; import { iswindow, isworker } from "..";
import { ScramjetClient } from "../client"; import { ScramjetClient } from "../client";
import { config } from "../shared";
export default function (client: ScramjetClient, self: typeof globalThis) { 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 // 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 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! // 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) { value: function (identifier: any, args: any) {
if (args && typeof args === "object" && args.length === 0) if (args && typeof args === "object" && args.length === 0)
for (const arg of args) { for (const arg of args) {
@ -58,7 +59,7 @@ export default function (client: ScramjetClient, self: typeof globalThis) {
// ((t)=>$scramjet$tryset(location,"+=",t)||location+=t)(...); // ((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 // 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 // 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) { value: function (lhs: any, op: string, rhs: any) {
if (lhs instanceof Location) { if (lhs instanceof Location) {
// @ts-ignore // @ts-ignore

View file

@ -1,6 +1,7 @@
import { encodeUrl } from "../shared/rewriters/url"; import { encodeUrl } from "../shared/rewriters/url";
import { ScramjetClient } from "./client"; import { ScramjetClient } from "./client";
import { wrapfn } from "."; import { indirectEval } from "./shared/eval";
import { config } from "./shared";
export function createWindowProxy( export function createWindowProxy(
client: ScramjetClient, client: ScramjetClient,
@ -9,16 +10,17 @@ export function createWindowProxy(
return new Proxy(self, { return new Proxy(self, {
get(target, prop) { get(target, prop) {
const propIsString = typeof prop === "string"; const propIsString = typeof prop === "string";
if (propIsString && prop === "location") { if (prop === "location") return client.locationProxy;
return client.locationProxy;
} else if ( if (
propIsString && propIsString &&
["window", "top", "self", "globalThis", "parent"].includes(prop) ["window", "top", "self", "globalThis", "parent"].includes(prop)
) { )
return self[wrapfn](self[prop]); return self[config.wrapfn](self[prop]);
} else if (propIsString && prop === "$scramjet") {
return; if (prop === "$scramjet") return;
}
if (prop === "eval") return indirectEval.bind(client);
const value = Reflect.get(target, prop); const value = Reflect.get(target, prop);

View file

@ -5,6 +5,10 @@ if (!self.$scramjet) {
self.$scramjet.config = { self.$scramjet.config = {
prefix: "/scramjet/", prefix: "/scramjet/",
codec: self.$scramjet.codecs.plain, codec: self.$scramjet.codecs.plain,
wrapfn: "$scramjet$wrap",
trysetfn: "$scramjet$tryset",
importfn: "$scramjet$import",
rewritefn: "$scramjet$rewrite",
config: "/scram/scramjet.config.js", config: "/scram/scramjet.config.js",
shared: "/scram/scramjet.shared.js", shared: "/scram/scramjet.shared.js",
worker: "/scram/scramjet.worker.js", worker: "/scram/scramjet.worker.js",

View file

@ -24,25 +24,21 @@ export function rewriteJs(js: string | ArrayBuffer, origin?: URL) {
if ("window" in globalThis) origin ??= new URL(decodeUrl(location.href)); if ("window" in globalThis) origin ??= new URL(decodeUrl(location.href));
const before = performance.now(); 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") { if (typeof js === "string") {
js = new TextDecoder().decode( js = new TextDecoder().decode(rewrite_js(js, origin.toString(), cfg));
rewrite_js(
js,
origin.toString(),
self.$scramjet.config.prefix,
self.$scramjet.config.codec.encode as any,
"$scramjet$wrap",
"$scramjet$import"
)
);
} else { } else {
js = rewrite_js_from_arraybuffer( js = rewrite_js_from_arraybuffer(
new Uint8Array(js), new Uint8Array(js),
origin.toString(), origin.toString(),
self.$scramjet.config.prefix, cfg
self.$scramjet.config.codec.encode as any,
"$scramjet$wrap",
"$scramjet$import"
); );
} }
const after = performance.now(); const after = performance.now();

4
src/types.d.ts vendored
View file

@ -34,6 +34,10 @@ declare global {
config: { config: {
prefix: string; prefix: string;
codec: Codec; codec: Codec;
wrapfn: string;
trysetfn: string;
importfn: string;
rewritefn: string;
config: string; config: string;
shared: string; shared: string;
worker: string; worker: string;