Skip to content

Commit 31dc00b

Browse files
committed
fix(cli): escape the prefix embedded in the Remote Control bootstrap script
JSON.stringify leaves `</script>` and the U+2028/U+2029 line terminators intact, so a relay path containing either could close the injected script element or break its string literal.
1 parent 76e97d5 commit 31dc00b

2 files changed

Lines changed: 25 additions & 1 deletion

File tree

apps/pythinker-code/src/cli/sub/web/remote-control.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ export function rewriteRemoteControlResponse(
289289
): Buffer {
290290
const normalizedPrefix = publicPrefix.replace(/\/+$/, '');
291291
if (contentType.toLowerCase().includes('text/html')) {
292-
const prefixLiteral = JSON.stringify(normalizedPrefix);
292+
const prefixLiteral = scriptStringLiteral(normalizedPrefix);
293293
const injected = `<script>(function(){var p=${prefixLiteral};try{sessionStorage.setItem('pythinker-desktop-server-origin',location.origin+p)}catch(e){}var w=function(f){return function(s,t,u){if(typeof u==='string'&&u.charAt(0)==='/'&&u.indexOf(p)!==0)u=p+u;return f.apply(this,[s,t,u])}};history.pushState=w(history.pushState);history.replaceState=w(history.replaceState)})();</script>`;
294294
let text = body.toString('utf8');
295295
const headMatch = /<head(?:\s[^>]*)?>/i.exec(text);
@@ -316,6 +316,19 @@ export function rewriteRemoteControlResponse(
316316
return body;
317317
}
318318

319+
/**
320+
* Embed a value in an inline `<script>`. `JSON.stringify` alone is not enough:
321+
* `</script>` in the value would close the element, and U+2028/U+2029 are line
322+
* terminators inside a JavaScript string literal.
323+
*/
324+
function scriptStringLiteral(value: string): string {
325+
return JSON.stringify(value)
326+
.replaceAll('<', '\\u003c')
327+
.replaceAll('>', '\\u003e')
328+
.replaceAll('\u2028', '\\u2028')
329+
.replaceAll('\u2029', '\\u2029');
330+
}
331+
319332
export async function startRemoteControl(
320333
options: RemoteControlOptions,
321334
): Promise<RemoteControlHandle> {

apps/pythinker-code/test/cli/web/remote-control.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ describe('Remote Control HTTP forwarding', () => {
138138
).toThrow(SyntaxError);
139139
});
140140

141+
it('escapes the injected prefix so it cannot close the script element', () => {
142+
const html = rewriteRemoteControlResponse(
143+
'text/html',
144+
Buffer.from('<html><head></head><body></body></html>'),
145+
'/relay</script><script>alert(1)</script>/devices/d1',
146+
).toString();
147+
expect(html).not.toContain('</script><script>alert(1)');
148+
expect(html).toContain('\\u003c/script\\u003e');
149+
expect(html.match(/<script>/g)).toHaveLength(1);
150+
});
151+
141152
it('rejects absolute-form and malformed request targets', () => {
142153
expect(() =>
143154
parseRawHttpRequest(Buffer.from('GET https://example.test/ HTTP/1.1\r\n\r\n')),

0 commit comments

Comments
 (0)