Sanitize account check IPC statuses

Centralize DebridAccountStatus sanitizing for direct account-check responses. The shared sanitizer collects stored and submitted credential variants, including raw, trimmed, URL-encoded, URL-decoded, full credential lines, and login:secret forms, then redacts credential-like query params, key-value echoes, authorization, cookie, API-key, token, password, secret, session, backup passphrase, and archive password text before any status DTO reaches the renderer.

Apply the sanitizer to bulk checkDebridAccounts results, single checkAccountCredentials results, and account command credential checks before returning, throwing, or persisting statuses. Reuse the same sanitizer for renderer snapshots so status redaction stays on one path.

Replace the Debrid-Link key popup copy action with a truthful non-secret masked-identity copy action and cover the regression so no renderer path copies key.token or reports a secret-copy success without a secret readback.
This commit is contained in:
Sucukdeluxe
2026-08-11 23:47:49 +02:00
parent 26d62a9337
commit 15f53f9c92
6 changed files with 233 additions and 51 deletions
+91
View File
@@ -0,0 +1,91 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { AppController } from "../src/main/app-controller";
import { defaultSettings } from "../src/main/constants";
import type { AppSettings, DebridAccountStatus } from "../src/shared/types";
vi.mock("electron", () => ({
app: { getPath: () => "C:\\MDD\\Test" },
BrowserWindow: class {},
clipboard: {},
dialog: {},
ipcMain: { handle: vi.fn(), on: vi.fn() },
Menu: { buildFromTemplate: vi.fn(), setApplicationMenu: vi.fn() },
safeStorage: { isEncryptionAvailable: () => false, encryptString: vi.fn(), decryptString: vi.fn() },
shell: {},
Tray: class {}
}));
function mockFetchOnce(status: number, body: unknown): void {
const text = typeof body === "string" ? body : JSON.stringify(body);
vi.stubGlobal("fetch", vi.fn(async () => ({
ok: status >= 200 && status < 300,
status,
text: async () => text
})) as unknown as typeof fetch);
}
function createController(settings: AppSettings): AppController {
const controller = Object.create(AppController.prototype) as {
settings: AppSettings;
manager: { applyDebridAccountStatuses: ReturnType<typeof vi.fn> };
audit: ReturnType<typeof vi.fn>;
};
controller.settings = settings;
controller.manager = { applyDebridAccountStatuses: vi.fn() };
controller.audit = vi.fn();
return controller as unknown as AppController;
}
function expectNoSecret(payload: unknown, secrets: readonly string[]): void {
const serialized = JSON.stringify(payload);
for (const secret of secrets) {
expect(serialized).not.toContain(secret);
}
}
afterEach(() => {
vi.unstubAllGlobals();
});
describe("account-check IPC result sanitizing", () => {
it("sanitizes bulk check statuses before returning or persisting them", async () => {
const login = "bulk-status@example.test";
const secret = "bulk raw secret+ä?=1";
const encodedSecret = encodeURIComponent(secret);
const credentialLine = `${login}:${secret}`;
const settings = {
...defaultSettings(),
megaCredentials: credentialLine,
megaDebridApiCredentials: credentialLine,
megaDebridApiEnabled: true
};
const providerText = `Denied https://www.mega-debrid.eu/api.php?action=connectUser&login=${encodeURIComponent(login)}&password=${encodedSecret} Authorization: Bearer ${encodedSecret} raw ${secret} line ${credentialLine}`;
mockFetchOnce(200, { response_code: "error", response_text: providerText });
const controller = createController(settings);
const statuses = await controller.checkDebridAccounts();
expectNoSecret(statuses, [secret, encodedSecret, credentialLine]);
expectNoSecret((controller as unknown as { manager: { applyDebridAccountStatuses: ReturnType<typeof vi.fn> } }).manager.applyDebridAccountStatuses.mock.calls, [secret, encodedSecret, credentialLine]);
expect(statuses[0]?.message).toContain("[geschützt]");
});
it("sanitizes single credential checks with raw, encoded, header and query echoes", async () => {
const secret = "single raw token+ö?=2";
const encodedSecret = encodeURIComponent(secret);
const echoedHeaderSecret = "provider-header-token-95K";
const echoedPassphrase = "provider-backup-passphrase-54A";
const providerText = `Rejected https://debrid-link.com/api/v2/account/infos?access_token=${encodedSecret}&apiKey=${secret} Authorization: Bearer ${encodedSecret} Cookie: sid=${secret} X-Api-Key: ${echoedHeaderSecret}
Backup-Passphrase=${echoedPassphrase}`;
mockFetchOnce(200, { success: false, error: providerText });
const controller = createController(defaultSettings());
const status = await controller.checkAccountCredentials({
kind: "debridlink-api",
secret
});
expectNoSecret(status, [secret, encodedSecret, echoedHeaderSecret, echoedPassphrase]);
expect((status as DebridAccountStatus).message).toContain("[geschützt]");
});
});
+3
View File
@@ -13,6 +13,9 @@ describe("desktop shell", () => {
expect(source).not.toMatch(/<span[^>]*className="[^"]*link-popup-click/);
expect(source.match(/<button[^>]*className="[^"]*link-popup-click[^>]*type="button"/g)).toHaveLength(3);
expect(source).not.toContain("navigator.clipboard.writeText(key.token)");
expect(source).toContain("navigator.clipboard.writeText(key.masked)");
expect(source).toContain("Maskierte Kennung kopiert");
});
it("confirms before removing a collector tab", () => {