mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
fix(electron): strip credentials on same-host https-to-http downgrades
Review follow-up (Greptile P1 + Codex on #1322): the host-only check kept Authorization/Cookie/basic auth/params/body when an https request was redirected to http on the same host, replaying a TLS-obtained session in cleartext. Treat that downgrade like a host change: strip credentials and refuse to replay request bodies. Scheme upgrades and port moves on the same host keep headers — the actual #1158 scenarios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87058c2f6d
commit
552923fd55
4 files changed
+78
-18
No files matched your search
@@ -4,8 +4,8 @@ area: stalker
|
||||
issues: [1158]
|
||||
---
|
||||
|
||||
Stalker portals whose server redirects between http/https or between ports no
|
||||
longer lose their session mid-request. Since 0.22 such redirects silently
|
||||
dropped the portal's MAC cookie and auth token, so categories failed to load
|
||||
and streams never reached any player; the session now survives every redirect
|
||||
that stays on the portal's own host.
|
||||
Stalker portals whose server redirects to https or to another port no longer
|
||||
lose their session mid-request. Since 0.22 such redirects silently dropped the
|
||||
portal's MAC cookie and auth token, so categories failed to load and streams
|
||||
never reached any player. Downgrade redirects from https to plain http still
|
||||
strip credentials, so a secure session is never sent in cleartext.
|
||||
@@ -403,7 +403,7 @@ describe('requestWithValidatedRedirects', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('replays the request body on a same-host scheme-change redirect', async () => {
|
||||
it('replays the request body on a same-host scheme-upgrade redirect', async () => {
|
||||
axiosMock
|
||||
.mockResolvedValueOnce({
|
||||
status: 307,
|
||||
@@ -425,6 +425,58 @@ describe('requestWithValidatedRedirects', () => {
|
||||
expect(axiosMock.mock.calls[1][0].data).toEqual({ payload: true });
|
||||
});
|
||||
|
||||
it('strips sensitive headers when a same-host redirect downgrades https to http', async () => {
|
||||
axiosMock
|
||||
.mockResolvedValueOnce({
|
||||
status: 302,
|
||||
headers: { location: 'http://portal.example/portal.php' },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
headers: {},
|
||||
data: { js: {} },
|
||||
});
|
||||
|
||||
await requestWithValidatedRedirects(
|
||||
'https://portal.example/portal.php',
|
||||
{
|
||||
auth: { password: 'secret', username: 'provider' },
|
||||
headers: {
|
||||
Authorization: 'Bearer secret',
|
||||
Cookie: 'mac=00:1A:79:AA:BB:CC',
|
||||
Accept: '*/*',
|
||||
},
|
||||
method: 'GET',
|
||||
params: { token: 'secret' },
|
||||
},
|
||||
{ resolveHostname: publicResolver }
|
||||
);
|
||||
|
||||
// A session obtained over TLS must never be replayed in cleartext.
|
||||
const redirectedConfig = axiosMock.mock.calls[1][0];
|
||||
expect(redirectedConfig.headers).toMatchObject({ Accept: '*/*' });
|
||||
expect(redirectedConfig.headers).not.toHaveProperty('Authorization');
|
||||
expect(redirectedConfig.headers).not.toHaveProperty('Cookie');
|
||||
expect(redirectedConfig.auth).toBeUndefined();
|
||||
expect(redirectedConfig.params).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects a same-host https-to-http redirect that would replay a request body', async () => {
|
||||
axiosMock.mockResolvedValueOnce({
|
||||
status: 307,
|
||||
headers: { location: 'http://portal.example/submit' },
|
||||
});
|
||||
|
||||
await expect(
|
||||
requestWithValidatedRedirects(
|
||||
'https://portal.example/submit',
|
||||
{ data: { secret: true }, method: 'POST' },
|
||||
{ resolveHostname: publicResolver }
|
||||
)
|
||||
).rejects.toThrow(/request bodies/i);
|
||||
expect(axiosMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not forward axios params to a cross-host redirect', async () => {
|
||||
axiosMock
|
||||
.mockResolvedValueOnce({
|
||||
|
||||
@@ -240,13 +240,19 @@ export async function requestWithValidatedRedirects<T = unknown>(
|
||||
// Credentials are scoped to the host, not the origin: IPTV portals
|
||||
// routinely redirect between schemes and ports of the same host
|
||||
// (http -> https upgrades, port moves), and stripping the session
|
||||
// cookie/token there breaks the portal outright (#1158). A redirect to
|
||||
// a *different* host still loses Authorization/Cookie/basic
|
||||
// auth/params so provider credentials never leak to third parties.
|
||||
if (nextUrl.hostname !== validatedUrl.hostname) {
|
||||
// cookie/token there breaks the portal outright (#1158). Two hops do
|
||||
// lose Authorization/Cookie/basic auth/params: a *different* host
|
||||
// (credentials never leak to third parties) and a same-host
|
||||
// https -> http downgrade (a session obtained over TLS is never
|
||||
// replayed in cleartext).
|
||||
const isCredentialUnsafeRedirect =
|
||||
nextUrl.hostname !== validatedUrl.hostname ||
|
||||
(validatedUrl.protocol === 'https:' &&
|
||||
nextUrl.protocol === 'http:');
|
||||
if (isCredentialUnsafeRedirect) {
|
||||
if (requestConfig.data !== undefined) {
|
||||
throw new UnsafeUrlError(
|
||||
'Cross-host redirects with request bodies are not supported',
|
||||
'Redirects that change the host or downgrade to HTTP cannot replay request bodies',
|
||||
502
|
||||
);
|
||||
}
|
||||
|
||||
@@ -164,13 +164,15 @@ validation while retaining the original hostname for TLS SNI, certificate
|
||||
validation, and virtual hosting. This prevents DNS rebinding between validation
|
||||
and connection. Callers with custom TLS policy provide a typed agent factory;
|
||||
the validated request layer supplies the pinned lookup instead of copying
|
||||
private Node `Agent.options` state. Cross-host redirects must not forward `Authorization`,
|
||||
`Cookie`, `Proxy-Authorization`, Axios `params`, or request bodies. Credential
|
||||
stripping is scoped to the host rather than the origin: redirects that stay on
|
||||
the same hostname but change scheme or port keep their headers, because IPTV
|
||||
portals routinely answer with http→https upgrades or port moves and losing the
|
||||
session cookie/token there breaks the portal outright (#1158) while disclosing
|
||||
nothing to a third party.
|
||||
private Node `Agent.options` state. Credential stripping is scoped to the host
|
||||
rather than the origin, with one transport-security carve-out: same-hostname
|
||||
redirects that upgrade the scheme (http→https) or move ports keep
|
||||
`Authorization`, `Cookie`, basic auth, `params`, and request bodies, because
|
||||
IPTV portals routinely answer with such redirects and losing the session
|
||||
cookie/token there breaks the portal outright (#1158) while disclosing nothing
|
||||
to a third party. Redirects that change the host **or downgrade https→http**
|
||||
must not forward any of those — the former would hand provider credentials to a
|
||||
third party, the latter would replay a TLS-obtained session in cleartext.
|
||||
|
||||
EPG URLs are strict by default because an M3U playlist can supply them through
|
||||
`url-tvg`. Operators who intentionally use a LAN-hosted EPG source should prefer
|
||||
|
||||
Reference in new issue
Block a user