Skip to content

Commit 353763c

Browse files
Copilothuntie
andcommitted
fix: use strict-url-sanitise to prevent RCE vulnerability (CVE-2025-11953)
Co-authored-by: huntie <2547783+huntie@users.noreply.github.com>
1 parent 1e3f67b commit 353763c

4 files changed

Lines changed: 84 additions & 9 deletions

File tree

packages/cli-server-api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"open": "^6.2.0",
1717
"pretty-format": "^29.7.0",
1818
"serve-static": "^1.13.1",
19+
"strict-url-sanitise": "0.0.1",
1920
"ws": "^6.2.3"
2021
},
2122
"devDependencies": {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import http from 'http';
2+
import open from 'open';
3+
import {openURLMiddleware} from '../openURLMiddleware';
4+
5+
jest.mock('open');
6+
jest.mock('strict-url-sanitise', () => ({
7+
sanitizeUrl: jest.fn((url: string) => {
8+
// Simulate the behavior of strict-url-sanitise
9+
const parsed = new URL(url);
10+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
11+
throw new Error(`Invalid url to pass to open(): ${url}`);
12+
}
13+
if (parsed.hostname !== encodeURIComponent(parsed.hostname)) {
14+
throw new Error(`Invalid url to pass to open(): ${url}`);
15+
}
16+
return url;
17+
}),
18+
}));
19+
20+
describe('openURLMiddleware', () => {
21+
let req: http.IncomingMessage & {body?: Object};
22+
let res: jest.Mocked<http.ServerResponse>;
23+
let next: jest.Mock;
24+
25+
beforeEach(() => {
26+
req = {
27+
method: 'POST',
28+
body: {},
29+
} as any;
30+
31+
res = {
32+
writeHead: jest.fn(),
33+
end: jest.fn(),
34+
} as any;
35+
36+
next = jest.fn();
37+
jest.clearAllMocks();
38+
});
39+
40+
afterEach(() => {
41+
jest.restoreAllMocks();
42+
});
43+
44+
it('should return 400 for non-string URL', async () => {
45+
req.body = {url: 123};
46+
47+
await openURLMiddleware(req, res, next);
48+
49+
expect(open).not.toHaveBeenCalled();
50+
expect(res.writeHead).toHaveBeenCalledWith(400);
51+
expect(res.end).toHaveBeenCalledWith('URL must be a string');
52+
});
53+
54+
it('should reject malicious URL with invalid hostname', async () => {
55+
const maliciousUrl = 'https://www.$(calc.exe).com/foo';
56+
req.body = {url: maliciousUrl};
57+
58+
await openURLMiddleware(req, res, next);
59+
60+
expect(open).not.toHaveBeenCalled();
61+
expect(res.writeHead).toHaveBeenCalledWith(400);
62+
expect(res.end).toHaveBeenCalledWith(
63+
expect.stringContaining('Invalid url to pass to open()'),
64+
);
65+
});
66+
});

packages/cli-server-api/src/openURLMiddleware.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import open from 'open';
1414
/**
1515
* Open a URL in the system browser.
1616
*/
17-
async function openURLMiddleware(
17+
export async function openURLMiddleware(
1818
req: IncomingMessage & {
1919
// Populated by body-parser
2020
body?: Object;
@@ -31,20 +31,23 @@ async function openURLMiddleware(
3131

3232
const {url} = req.body as {url: string};
3333

34+
if (typeof url !== 'string') {
35+
res.writeHead(400);
36+
res.end('URL must be a string');
37+
return;
38+
}
39+
40+
let sanitizedUrl: string;
3441
try {
35-
const parsedUrl = new URL(url);
36-
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
37-
res.writeHead(400);
38-
res.end('Invalid URL protocol');
39-
return;
40-
}
42+
const {sanitizeUrl} = await import('strict-url-sanitise');
43+
sanitizedUrl = sanitizeUrl(url);
4144
} catch (error) {
4245
res.writeHead(400);
43-
res.end('Invalid URL format');
46+
res.end(error instanceof Error ? error.message : 'Invalid URL');
4447
return;
4548
}
4649

47-
await open(url);
50+
await open(sanitizedUrl);
4851

4952
res.writeHead(200);
5053
res.end();

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9703,6 +9703,11 @@ stream-buffers@2.2.x:
97039703
resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4"
97049704
integrity sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==
97059705

9706+
strict-url-sanitise@0.0.1:
9707+
version "0.0.1"
9708+
resolved "https://registry.yarnpkg.com/strict-url-sanitise/-/strict-url-sanitise-0.0.1.tgz#10cfac63c9dfdd856d98ab9f76433dad5ce99e0c"
9709+
integrity sha512-nuFtF539K8jZg3FjaWH/L8eocCR6gegz5RDOsaWxfdbF5Jqr2VXWxZayjTwUzsWJDC91k2EbnJXp6FuWW+Z4hg==
9710+
97069711
string-argv@^0.3.1:
97079712
version "0.3.1"
97089713
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"

0 commit comments

Comments
 (0)