Skip to content

Commit 5cc0f9c

Browse files
committed
feat: enable inline event handler execution and update module identity to include runtime and tab tokens
1 parent a3aa1cc commit 5cc0f9c

7 files changed

Lines changed: 191 additions & 63 deletions

File tree

rewriter-rs/src/html/document.rs

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ use std::{
66

77
use lol_html::{element, end, html_content::ContentType, rewrite_str, text, RewriteStrSettings};
88

9-
use crate::{css, import_map, js, share_url, RewriteContext};
9+
use crate::{
10+
css, import_map, js, rewrite_wrapped_source, share_url, RewriteContext, RewriteOutput,
11+
};
1012

1113
use super::{attr_policy_kind, fetch_url, link_rel_kind, srcset, target_url as resolve_target_url};
1214
use super::{blocked_element_kind, event_handler_attr_kind, meta_policy_kind, script_type_kind};
@@ -161,6 +163,7 @@ fn rewrite_element_attrs<H: lol_html::HandlerTypes>(
161163
if tag == "link" {
162164
backup_masked_attrs(el, false)?;
163165
}
166+
rewrite_event_handler_attrs(el, target_url, control_prefix)?;
164167
if tag == "base" {
165168
rewrite_base_element(el, target_url, control_prefix)?;
166169
return Ok(());
@@ -172,7 +175,6 @@ fn rewrite_element_attrs<H: lol_html::HandlerTypes>(
172175
rewrite_link_attrs(el, target_url, control_prefix)?;
173176
return Ok(());
174177
}
175-
rewrite_event_handler_attrs(el)?;
176178
rewrite_inline_style_attr(el, target_url, control_prefix)?;
177179
rewrite_srcdoc_attr(el, &tag, runtime_prelude)?;
178180
for attr in ["href", "xlink:href", "src", "poster"] {
@@ -214,6 +216,8 @@ fn rewrite_base_element<H: lol_html::HandlerTypes>(
214216

215217
fn rewrite_event_handler_attrs<H: lol_html::HandlerTypes>(
216218
el: &mut lol_html::html_content::Element<'_, '_, H>,
219+
target_url: &str,
220+
control_prefix: &str,
217221
) -> lol_html::HandlerResult {
218222
let handlers = attr_names(el)
219223
.into_iter()
@@ -222,11 +226,28 @@ fn rewrite_event_handler_attrs<H: lol_html::HandlerTypes>(
222226
for name in handlers {
223227
let value = el.get_attribute(&name).unwrap_or_default();
224228
el.remove_attribute(&name);
225-
el.set_attribute(&format!("data-zp-blocked-{name}"), &value)?;
229+
let rewritten = rewrite_event_handler(&value, target_url, control_prefix);
230+
if rewritten.ok {
231+
el.set_attribute(&format!("data-zp-event-{name}"), &rewritten.code)?;
232+
} else {
233+
el.set_attribute(&format!("data-zp-blocked-{name}"), &value)?;
234+
}
226235
}
227236
Ok(())
228237
}
229238

239+
fn rewrite_event_handler(source: &str, target_url: &str, control_prefix: &str) -> RewriteOutput {
240+
let ctx = RewriteContext::new(target_url, control_prefix, "", "");
241+
rewrite_wrapped_source(
242+
source,
243+
"function __zp_event__(event){\n",
244+
"\n}",
245+
false,
246+
ctx.without_runtime_context(),
247+
true,
248+
)
249+
}
250+
230251
fn rewrite_inline_style_attr<H: lol_html::HandlerTypes>(
231252
el: &mut lol_html::html_content::Element<'_, '_, H>,
232253
target_url: &str,
@@ -461,6 +482,11 @@ fn escape_inline_script_sentinel(code: &str) -> String {
461482
fn drop_control_attrs<H: lol_html::HandlerTypes>(
462483
el: &mut lol_html::html_content::Element<'_, '_, H>,
463484
) {
485+
for attr in attr_names(el) {
486+
if attr.to_ascii_lowercase().starts_with("data-zp-event-") {
487+
el.remove_attribute(&attr);
488+
}
489+
}
464490
for attr in [
465491
"data-zp-target-url",
466492
"data-zp-target-srcset",
@@ -862,7 +888,7 @@ mod tests {
862888
#[test]
863889
fn rewrites_link_policy_with_lol_html() {
864890
let out = rewrite_document(
865-
r#"<head><link rel="preconnect" href="https://cdn.example/"><link rel="icon" href="/favicon.ico"><link rel="apple-touch-icon" href="touch.png"><link rel="stylesheet" href="/app.css"><link rel="stylesheet" href="data:text/css,x"></head>"#,
891+
r#"<head><link rel="preconnect" href="https://cdn.example/"><link rel="icon" href="/favicon.ico"><link rel="apple-touch-icon" href="touch.png"><link rel="stylesheet" media="print" onload="this.media='all'; this.onload=null;" href="/app.css"><link rel="stylesheet" href="data:text/css,x"></head>"#,
866892
DocumentOptions {
867893
target_url: "https://example.com/app/page.html",
868894
control_prefix: "/zp/",
@@ -882,6 +908,8 @@ mod tests {
882908
r#"data-zp-target-url="https://example.com/app/touch.png""#,
883909
r#"href="/zp/api/fetch?url=https%3A%2F%2Fexample.com%2Fapp.css""#,
884910
r#"data-zp-target-url="https://example.com/app.css""#,
911+
r#"data-zp-event-onload=""#,
912+
r#"__zp_runEvent"#,
885913
r#"href="/zp/error/POLICY_BLOCKED""#,
886914
r#"data-zp-blocked-url="data:text/css,x""#,
887915
] {
@@ -894,6 +922,8 @@ mod tests {
894922
r#"href="/favicon.ico""#,
895923
r#"href="touch.png""#,
896924
r#"href="/app.css""#,
925+
r#" onload="#,
926+
r#"data-zp-blocked-onload"#,
897927
] {
898928
assert!(
899929
!out.contains(forbidden),
@@ -1056,7 +1086,7 @@ mod tests {
10561086
fn rewrites_script_style_importmap_and_srcdoc_with_lol_html() {
10571087
let prelude = r#"<script nonce=zp>boot()</script><script nonce=zp src="/zp/assets/runtime-prelude.js"></script>"#;
10581088
let out = rewrite_document(
1059-
r#"<body onload="location.href='/boot'"><script src="/app.js" integrity="sha384-i" nonce="target-nonce"></script><script>window.location.href="<\/script>";</script><script type="module">import "./dep.js"; window.location.href;</script><script type="importmap">{"imports":{"a":"./a.js"}}</script><style>body{background:url("/bg.png")}</style><button onclick="return location.href"></button><iframe srcdoc="<p>x</p>"></iframe></body>"#,
1089+
r#"<body onload="location.href='/boot'"><script src="/app.js" integrity="sha384-i" nonce="target-nonce"></script><script type="module" src="/entry.js"></script><script>window.location.href="<\/script>";</script><script type="module">import "./dep.js"; window.location.href;</script><script type="importmap">{"imports":{"a":"./a.js"}}</script><style>body{background:url("/bg.png")}</style><button onclick="return location.href"></button><iframe srcdoc="<p>x</p>"></iframe></body>"#,
10601090
DocumentOptions {
10611091
target_url: "https://example.com/app/page.html",
10621092
control_prefix: "/zp/",
@@ -1070,7 +1100,9 @@ mod tests {
10701100

10711101
for want in [
10721102
r#"src="/zp/api/script?kind=classic&u=https%3A%2F%2Fexample.com%2Fapp.js&tab=tab-1&rt=rt-1""#,
1103+
r#"src="/zp/api/script?kind=module&u=https%3A%2F%2Fexample.com%2Fentry.js&tab=tab-1&rt=rt-1""#,
10731104
r#"data-zp-target-url="https://example.com/app.js""#,
1105+
r#"data-zp-target-url="https://example.com/entry.js""#,
10741106
r#"data-zp-integrity="sha384-i""#,
10751107
r#"data-zp-target-nonce="target-nonce""#,
10761108
r#"nonce="zp""#,
@@ -1079,8 +1111,9 @@ mod tests {
10791111
r#"/zp/api/script?kind=module&u=https%3A%2F%2Fexample.com%2Fapp%2Fdep.js&tab=tab-1&rt=rt-1"#,
10801112
r#""a":"/zp/api/script?kind=module\u0026rt=rt-1\u0026tab=tab-1\u0026u=https%3A%2F%2Fexample.com%2Fapp%2Fa.js""#,
10811113
r#"url("/zp/api/fetch?url=https%3A%2F%2Fexample.com%2Fbg.png")"#,
1082-
r#"data-zp-blocked-onload="location.href='/boot'""#,
1083-
r#"data-zp-blocked-onclick="return location.href""#,
1114+
r#"data-zp-event-onload=""#,
1115+
r#"data-zp-event-onclick=""#,
1116+
r#"__zp_runEvent"#,
10841117
r#"srcdoc="<script nonce=zp>boot()</script>"#,
10851118
] {
10861119
assert!(out.contains(want), "missing {want} in {out}");
@@ -1089,6 +1122,8 @@ mod tests {
10891122
for forbidden in [
10901123
r#" onload="#,
10911124
r#" onclick="#,
1125+
r#"data-zp-blocked-onload"#,
1126+
r#"data-zp-blocked-onclick"#,
10921127
r#" integrity="sha384-i""#,
10931128
r#" nonce="target-nonce""#,
10941129
r#"src="/app.js""#,

rewriter-rs/src/js/module_urls.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,8 @@ pub(crate) fn script_url(
6868
normalized_kind,
6969
percent_encode(abs.clone())
7070
);
71-
if normalized_kind != "module" {
72-
append_context(&mut out, "tab", tab_id);
73-
append_context(&mut out, "rt", runtime_token);
74-
}
71+
append_context(&mut out, "tab", tab_id);
72+
append_context(&mut out, "rt", runtime_token);
7573
ScriptURL {
7674
ok: true,
7775
url: out,
@@ -297,7 +295,7 @@ mod tests {
297295
assert!(module.ok);
298296
assert_eq!(
299297
module.url,
300-
"/zp/api/script?kind=module&u=https%3A%2F%2Ftarget.example%2Fmain.js"
298+
"/zp/api/script?kind=module&u=https%3A%2F%2Ftarget.example%2Fmain.js&tab=tab&rt=rt"
301299
);
302300
}
303301

rewriter-rs/src/js/swc_rewriter.rs

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ impl VisitMut for SwcRewriter<'_> {
206206
for arg in &mut call.args {
207207
arg.expr.visit_mut_with(self);
208208
}
209-
let args = array_expr(call.args.iter().cloned().map(expr_from_spread).collect());
209+
let args = array_expr_from_args(call.args.clone());
210210
*call = call_expr("__zp_call", vec![base, prop, args]);
211211
return;
212212
}
@@ -285,13 +285,11 @@ impl VisitMut for SwcRewriter<'_> {
285285
arg.expr.visit_mut_with(self);
286286
}
287287
}
288-
let args = array_expr(
289-
new_expr
290-
.args
291-
.as_ref()
292-
.map(|args| args.iter().cloned().map(expr_from_spread).collect())
293-
.unwrap_or_default(),
294-
);
288+
let args = new_expr
289+
.args
290+
.as_ref()
291+
.map(|args| array_expr_from_args(args.clone()))
292+
.unwrap_or_else(|| array_expr(Vec::new()));
295293
*expr = call_helper("__zp_construct", vec![callee, args]);
296294
return;
297295
}
@@ -670,11 +668,11 @@ impl SwcRewriter<'_> {
670668

671669
fn rewrite_optional_call(&mut self, call: &OptCall) -> Option<Expr> {
672670
let (base, prop) = self.optional_call_target_parts(&call.callee)?;
673-
let args = array_expr(
671+
let args = array_expr_from_args(
674672
call.args
675673
.iter()
676674
.cloned()
677-
.map(|arg| self.transformed_arg_expr(arg))
675+
.map(|arg| self.transformed_arg(arg))
678676
.collect(),
679677
);
680678
Some(call_helper("__zp_optionalCall", vec![base, prop, args]))
@@ -699,14 +697,14 @@ impl SwcRewriter<'_> {
699697
}
700698
}
701699

702-
fn transformed_arg_expr(&mut self, arg: ExprOrSpread) -> Expr {
700+
fn transformed_arg(&mut self, arg: ExprOrSpread) -> ExprOrSpread {
703701
let ExprOrSpread { spread, expr } = arg;
704702
let mut expr = *expr;
705703
expr.visit_mut_with(self);
706-
expr_from_spread(ExprOrSpread {
704+
ExprOrSpread {
707705
spread,
708706
expr: Box::new(expr),
709-
})
707+
}
710708
}
711709
}
712710

@@ -763,15 +761,11 @@ fn array_expr(values: Vec<Expr>) -> Expr {
763761
})
764762
}
765763

766-
fn expr_from_spread(arg: ExprOrSpread) -> Expr {
767-
if arg.spread.is_some() {
768-
Expr::Array(ArrayLit {
769-
span: DUMMY_SP,
770-
elems: vec![Some(arg)],
771-
})
772-
} else {
773-
*arg.expr
774-
}
764+
fn array_expr_from_args(args: Vec<ExprOrSpread>) -> Expr {
765+
Expr::Array(ArrayLit {
766+
span: DUMMY_SP,
767+
elems: args.into_iter().map(Some).collect(),
768+
})
775769
}
776770

777771
fn call_expr(name: &str, args: Vec<Expr>) -> swc_ecma_ast::CallExpr {

rewriter-rs/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ fn rewrite_program_source(source: &str, module: bool, ctx: RewriteContext<'_>) -
338338
}
339339
}
340340

341-
fn rewrite_wrapped_source(
341+
pub(crate) fn rewrite_wrapped_source(
342342
source: &str,
343343
prefix: &str,
344344
suffix: &str,
@@ -445,14 +445,16 @@ mod tests {
445445
#[test]
446446
fn rewrites_calls_and_constructors() {
447447
let code = rewrite_ok(
448-
"window.location = '/next'; const ws = new WebSocket('/ws', ['chat']); Object.getOwnPropertyDescriptor(window, 'location');",
448+
"window.location = '/next'; const ws = new WebSocket('/ws', ['chat']); Object.getOwnPropertyDescriptor(window, 'location'); Object.assign(target, ...sources); new WebSocket(...wsArgs);",
449449
"classic",
450450
"https://example.com/app.js",
451451
);
452452
assert!(code.contains("__zp_set(__zp_get(globalThis,\"window\"),\"location\",\"/next\")"));
453453
assert!(code
454454
.contains("__zp_construct(__zp_get(globalThis,\"WebSocket\"),[\"/ws\",[\"chat\"]])"));
455+
assert!(code.contains("__zp_construct(__zp_get(globalThis,\"WebSocket\"),[...wsArgs])"));
455456
assert!(code.contains("__zp_call(Object,\"getOwnPropertyDescriptor\",[__zp_get(globalThis,\"window\"),\"location\"])"));
457+
assert!(code.contains("__zp_call(Object,\"assign\",[target,...sources])"));
456458
}
457459

458460
#[test]

test/js/rewriter.test.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,10 @@ test('Rust rewriter asset owns external script URL rewriting', async () => {
432432
controlPrefix: '/zp/',
433433
});
434434
assert.equal(module.ok, true, JSON.stringify(module.diagnostics));
435-
assert.equal(module.url, '/zp/api/script?kind=module&u=https%3A%2F%2Fexample.com%2Fmain.js');
435+
assert.equal(
436+
module.url,
437+
'/zp/api/script?kind=module&u=https%3A%2F%2Fexample.com%2Fmain.js&tab=tab-1&rt=rt-1',
438+
);
436439

437440
const blocked = ctx.ZPRewriter.rewriteScriptURL('data:text/javascript,0', {
438441
kind: 'classic',
@@ -906,6 +909,41 @@ test('Rust rewriter preserves optional access semantics for guarded probes', asy
906909
);
907910
});
908911

912+
test('Rust rewriter preserves spread call arguments passed through helpers', async () => {
913+
const rewriter = await loadRewriter();
914+
const out = rewriter.rewriteScript(
915+
`
916+
const sources = [{ reducer: 1 }, { middleware() { return 'ok'; } }];
917+
const target = {};
918+
Object.assign(target, ...sources);
919+
window.result = [target.reducer, typeof target.middleware, target.middleware()];
920+
`,
921+
{
922+
kind: 'classic',
923+
targetUrl: 'https://widgets.example/assets/api.js',
924+
},
925+
);
926+
assert.equal(out.ok, true, JSON.stringify(out.diagnostics));
927+
assertCodeIncludes(out.code, '__zp_call(Object,"assign",[target,...sources])');
928+
929+
const ctx = {
930+
Object,
931+
window: {},
932+
globalThis: null,
933+
__zp_get: (base, prop) => base[prop],
934+
__zp_set: (base, prop, value) => {
935+
base[prop] = value;
936+
return value;
937+
},
938+
__zp_call: (base, prop, args) => Reflect.apply(base[prop], base, args),
939+
};
940+
ctx.globalThis = ctx;
941+
vm.runInNewContext(out.code, ctx);
942+
assert.equal(ctx.window.result[0], 1);
943+
assert.equal(ctx.window.result[1], 'function');
944+
assert.equal(ctx.window.result[2], 'ok');
945+
});
946+
909947
test('Rust rewriter routes computed global-alias member access through runtime membrane', async () => {
910948
const rewriter = await loadRewriter();
911949
const out = rewriter.rewriteScript(

test/js/static-policy.test.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,10 @@ test('runtime installs required escape-vector hooks', () => {
232232
'shouldContainFrameWindow && shouldContainFrameWindow(this, childWin)',
233233
'installRequestFacade',
234234
'return new Native.Request(requestLike ? input : requestTargetURL(input), init)',
235-
"Native.setAttribute.call(this, k, '')",
235+
'eventHandlerBindings',
236+
'data-zp-event-',
237+
"Native.FunctionCtor('event'",
238+
'bindEventAttribute',
236239
"'WebSocketStream'",
237240
'getUserMedia',
238241
'mediaDevices',
@@ -284,14 +287,18 @@ test('runtime keeps JavaScript rewriting fail-closed and canonicalizes module UR
284287
const start = rt.indexOf('function scriptProxyPath(target, kind)');
285288
const end = rt.indexOf('function setScriptSource', start);
286289
const body = rt.slice(start, end);
287-
assert.ok(body.includes("if (kind !== 'module')"), 'module proxy URLs must stay canonical');
290+
assert.ok(body.includes("if (kind !== 'module')"), 'module proxy URLs must keep referrer data out');
288291
assert.ok(
289-
body.indexOf("if (kind !== 'module')") < body.indexOf("params.set('ref'"),
290-
'ref/rp must not be part of module identity',
292+
body.indexOf("params.set('tab'") < body.indexOf("if (kind !== 'module')"),
293+
'runtime tab token must be part of module graph identity',
294+
);
295+
assert.ok(
296+
body.indexOf("params.set('rt'") < body.indexOf("if (kind !== 'module')"),
297+
'runtime token must be part of module graph identity',
291298
);
292299
assert.ok(
293-
body.indexOf("if (kind !== 'module')") < body.indexOf("params.set('tab'"),
294-
'runtime tab token must not be part of module identity',
300+
body.indexOf("if (kind !== 'module')") < body.indexOf("params.set('ref'"),
301+
'ref/rp must not be part of module identity',
295302
);
296303
});
297304

0 commit comments

Comments
 (0)