fix(pwa): sanitize portal URL before appending stalker cmd

A registered portal URL carrying a fragment would swallow the appended
cmd (everything after # is never transmitted), and a trailing bare '?'
produced '??cmd='. Drop the hash and pick the separator from the
sanitized href before appending. Flagged by Greptile/Codex on #1334.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-01 23:29:39 +02:00
1 parent 3326f73388
commit e0617e1642
2 files changed
+52 -3

No files matched your search

@@ -440,6 +440,44 @@ https://stream.example/live.m3u8`);
);
});
it('keeps cmd in the query when the registered portal URL carries a fragment or bare ?', async () => {
const httpClient = new StubHttpClient();
httpClient.queueResponse({ js: { cmd: 'http://cdn/a.m3u8' } });
httpClient.queueResponse({ js: { cmd: 'http://cdn/b.m3u8' } });
await withServer(
createWebBackendApp({
httpClient,
resolveHostname: resolvePublicHost,
}),
async (baseUrl) => {
// A fragment on the registered URL must not swallow the
// appended cmd (fragments are never sent to the portal).
const fragmentTarget = await registerProviderTarget(
baseUrl,
'http://stalker.example/portal.php#legacy'
);
await fetch(
`${baseUrl}/stalker?targetId=${fragmentTarget}&action=create_link&cmd=${encodeURIComponent('/media/1.mpg')}`
);
// A trailing bare '?' must not produce '??cmd='.
const bareQueryTarget = await registerProviderTarget(
baseUrl,
'http://stalker.example/load.php?'
);
await fetch(
`${baseUrl}/stalker?targetId=${bareQueryTarget}&action=create_link&cmd=${encodeURIComponent('/media/2.mpg')}`
);
expect(httpClient.requests.map((request) => request.url)).toEqual([
'http://stalker.example/portal.php?cmd=/media/1.mpg',
'http://stalker.example/load.php?cmd=/media/2.mpg',
]);
}
);
});
it('normalizes provider errors for portal proxy calls', async () => {
const httpClient = new StubHttpClient();
httpClient.queueFailure(403, 'Forbidden');
+14 -3
View File
@@ -233,10 +233,21 @@ export function createWebBackendApp(
// serializer would fully percent-encode it, diverging from what a
// real STB (and the Electron transport) sends. Append it to the
// URL with the shared encoder and let axios serialize the rest.
// The fragment is dropped first: a registered URL carrying `#...`
// would otherwise swallow the appended cmd, and a trailing bare
// `?` must not become `??cmd=`.
const { cmd, ...proxyParams } = getProxyParams(req, ['targetId']);
const requestUrl = cmd
? `${url.href}${url.search ? '&' : '?'}cmd=${encodeStalkerCmdValue(cmd)}`
: url.href;
const portalUrl = new URL(url.href);
portalUrl.hash = '';
let requestUrl = portalUrl.href;
if (cmd) {
const separator = requestUrl.endsWith('?')
? ''
: portalUrl.search
? '&'
: '?';
requestUrl = `${requestUrl}${separator}cmd=${encodeStalkerCmdValue(cmd)}`;
}
// Provider URLs are validated by /provider-targets before they enter the registry.
// codeql[js/request-forgery]