mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 13:16:16 -08:00
fix(sources): compare passwords in their own case; only a MAC caselessly
Matching every secret lower-cased hid any name holding the password word in another case: the Electron E2E source "Stress Xtream Portal" (password `stress`) showed its host, and the deletion test could not find its row. A password is case-sensitive, so it is compared as written; the text is still read fully decoded, so `%3a` escapes count as `:`. A MAC is the same address in either letter case and is compared lower-cased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c4da37ebb0
commit
c6d7b241d3
2 files changed
+42
-17
No files matched your search
@@ -148,9 +148,11 @@ describe('sourceDisplayName', () => {
|
||||
expect(sourceDisplayName({ ...xtream, title: 'pw' })).toBe(
|
||||
'xtream.tv:8080'
|
||||
);
|
||||
expect(sourceDisplayName({ ...xtream, title: ' PW ' })).toBe(
|
||||
expect(sourceDisplayName({ ...xtream, title: ' pw ' })).toBe(
|
||||
'xtream.tv:8080'
|
||||
);
|
||||
// A password has its own case: `PW` is not `pw`.
|
||||
expect(sourceDisplayName({ ...xtream, title: 'PW' })).toBe('PW');
|
||||
// Inside a longer name a short password is just letters.
|
||||
expect(sourceDisplayName({ ...xtream, title: 'Pwnova' })).toBe(
|
||||
'Pwnova'
|
||||
@@ -160,6 +162,17 @@ describe('sourceDisplayName', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps a name holding the password word in another case', () => {
|
||||
expect(
|
||||
sourceDisplayName({
|
||||
title: 'Stress Portal',
|
||||
serverUrl: 'http://xtream.tv',
|
||||
username: 'stress',
|
||||
password: 'stress',
|
||||
})
|
||||
).toBe('Stress Portal');
|
||||
});
|
||||
|
||||
it('keeps a name that only shares a letter with a decoded password', () => {
|
||||
// `%2542` is the password as written; decoded it is just `B`.
|
||||
const url = 'https://host.test/list?password=%2542';
|
||||
|
||||
@@ -199,28 +199,40 @@ function sourceSecrets(playlist: SourceDisplayFields): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the text shows any of the source's secrets. Escapes may be
|
||||
* written in either case (`%3a` for `%3A`) and a MAC in either letter case,
|
||||
* so text and secrets are compared lower-cased, the text as written and
|
||||
* fully decoded.
|
||||
* Whether one of the texts shows the secret: as the whole text, or inside
|
||||
* it when the secret is long enough. Each form on its own: `%2542` decodes
|
||||
* to `B`, which may only hide a text that is nothing but `B`.
|
||||
*/
|
||||
function shows(texts: readonly string[], secret: string): boolean {
|
||||
return texts.some(
|
||||
(text) =>
|
||||
text === secret ||
|
||||
(secret.length >= MIN_SECRET_LENGTH && text.includes(secret))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the text shows any of the source's secrets, read as written and
|
||||
* fully decoded (so `%3a` escapes count as `:`). A password is compared in
|
||||
* its own case: `Stress` in a name is not the password `stress`. A MAC is
|
||||
* the same address in either letter case, so it is compared lower-cased.
|
||||
*/
|
||||
export function containsSourceSecret(
|
||||
text: string,
|
||||
playlist: SourceDisplayFields
|
||||
): boolean {
|
||||
const forms = [text, decodeFully(text)].map((form) =>
|
||||
form.trim().toLowerCase()
|
||||
const texts = [text, decodeFully(text)].map((form) => form.trim());
|
||||
if (sourceSecrets(playlist).some((secret) => shows(texts, secret))) {
|
||||
return true;
|
||||
}
|
||||
const mac = playlist.macAddress?.toLowerCase();
|
||||
const lowerTexts = texts.map((form) => form.toLowerCase());
|
||||
return (
|
||||
!!mac &&
|
||||
[mac, encodeURIComponent(mac).toLowerCase()].some((form) =>
|
||||
shows(lowerTexts, form)
|
||||
)
|
||||
);
|
||||
return sourceSecrets(playlist).some((secret) => {
|
||||
const needle = secret.toLowerCase();
|
||||
// Each form on its own: `%2542` decodes to `B`, which may only hide
|
||||
// a text that is nothing but `b`.
|
||||
return forms.some(
|
||||
(form) =>
|
||||
form === needle ||
|
||||
(needle.length >= MIN_SECRET_LENGTH && form.includes(needle))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user