mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 20:56:19 -08:00
fix(playlist): mask HTTP Basic passwords on auto-detect cards
An M3U link can carry credentials as userinfo
('https://alice:secret@host/list.m3u'); the card masked query passwords only,
so that one showed in clear despite the card's masking guarantee. The
username stays visible and the candidate keeps the untouched URL for
prefilling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
This commit is contained in:
2 files changed
+58
No files matched your search
@@ -94,6 +94,21 @@ describe('AutoImportComponent', () => {
|
||||
expect(JSON.stringify(rows)).not.toContain('s3cret');
|
||||
});
|
||||
|
||||
it('masks an HTTP Basic password embedded in the URL', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const rows = component.summaryRows({
|
||||
kind: 'm3u-url',
|
||||
confidence: 'high',
|
||||
url: 'https://alice:s3cret@lists.example.com/list.m3u',
|
||||
});
|
||||
|
||||
expect(rows[0].value).toBe(
|
||||
'https://alice:••••••@lists.example.com/list.m3u'
|
||||
);
|
||||
expect(rows[0].value).not.toContain('s3cret');
|
||||
});
|
||||
|
||||
it('masks percent-encoded and repeated password parameters', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
|
||||
@@ -50,6 +50,49 @@ const CONFIDENCE_LABEL_KEYS: Record<ProviderImportConfidence, string> = {
|
||||
* the importer — reads as `password`.
|
||||
*/
|
||||
function maskUrlPasswords(url: string): string {
|
||||
return maskUrlQueryPasswords(maskUrlUserinfoPassword(url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the password half of HTTP Basic userinfo
|
||||
* (`https://alice:secret@host/…`), which is a password on the card exactly
|
||||
* like a query one. The username stays visible, as it does everywhere else.
|
||||
*/
|
||||
function maskUrlUserinfoPassword(url: string): string {
|
||||
const schemeEnd = url.indexOf('://');
|
||||
if (schemeEnd === -1) {
|
||||
return url;
|
||||
}
|
||||
const authorityStart = schemeEnd + 3;
|
||||
const authorityEnd = (() => {
|
||||
const rest = url.slice(authorityStart);
|
||||
const cut = rest.search(/[/?#]/);
|
||||
return cut === -1 ? url.length : authorityStart + cut;
|
||||
})();
|
||||
|
||||
const authority = url.slice(authorityStart, authorityEnd);
|
||||
// Last `@`: one inside the password itself has to be percent-encoded, so
|
||||
// whatever follows the final `@` is the host.
|
||||
const userinfoEnd = authority.lastIndexOf('@');
|
||||
if (userinfoEnd === -1) {
|
||||
return url;
|
||||
}
|
||||
const userinfo = authority.slice(0, userinfoEnd);
|
||||
const separator = userinfo.indexOf(':');
|
||||
if (separator === -1) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const masked = `${userinfo.slice(0, separator)}:••••••`;
|
||||
return (
|
||||
url.slice(0, authorityStart) +
|
||||
masked +
|
||||
authority.slice(userinfoEnd) +
|
||||
url.slice(authorityEnd)
|
||||
);
|
||||
}
|
||||
|
||||
function maskUrlQueryPasswords(url: string): string {
|
||||
const queryStart = url.indexOf('?');
|
||||
if (queryStart === -1) {
|
||||
return url;
|
||||
|
||||
Reference in new issue
Block a user