Compare commits

..
1 Commits
Author SHA1 Message Date
sneak c6d3890af1 harden: debug mode logs only a request's origin and JSON-RPC method (closes #410)
check / check (push) Failing after 3s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
With debug mode on, debugFetch logged every request's full URL and body,
so an RPC endpoint with an API key in its path or query string printed
that key to the console on every request. It now logs the HTTP method,
the URL's origin and, for a JSON-RPC body, the method name. The balance
refresh and token lookup, which logged the RPC URL at debug level, log
its origin too. The error lines for failed RPC calls print ethers' short
message, since its full message for an HTTP error carries the request
URL. The README's DEBUG Mode Policy says what debug mode logs.

Model: opus-5-5
2026-10-04 18:15:50 +00:00
7 changed files with 13 additions and 197 deletions
+3 -4
View File
@@ -2174,10 +2174,9 @@ request made through `debugFetch` in `src/shared/log.js` (the explorer, the
price feed, the RPC calls a site makes, and the endpoint checks in settings) and price feed, the RPC calls a site makes, and the endpoint checks in settings) and
for its response. A request is logged by its HTTP method, the origin of its URL for its response. A request is logged by its HTTP method, the origin of its URL
(scheme, host and port) and, for a JSON-RPC call, the method name; the balance (scheme, host and port) and, for a JSON-RPC call, the method name; the balance
refresh, the token lookup and a failed endpoint check in settings name the refresh and token lookup name the RPC endpoint by its origin too. The URL's path
endpoint by its origin too. The URL's path and query string, where RPC providers and query string, where RPC providers put API keys, and the request body are
put API keys, any user name and password in it, and the request body are never never logged.
logged.
### Key Decisions ### Key Decisions
+2 -5
View File
@@ -52,11 +52,8 @@ but the review is broader than any of them.
method, the URL's origin and, for a JSON-RPC call, the method name. The method, the URL's origin and, for a JSON-RPC call, the method name. The
balance refresh and token lookup log the RPC endpoint by its origin too. A balance refresh and token lookup log the RPC endpoint by its origin too. A
failed RPC call's error line prints the error's short message, which names the failed RPC call's error line prints the error's short message, which names the
HTTP status, not its full message, which carries the request URL. A failed HTTP status, not its full message, which carries the request URL. The README's
endpoint check in settings names the endpoint by its origin, not the `fetch` DEBUG Mode Policy says what debug mode logs.
error's message, which carries the whole URL, password included, for a URL
with a user name and password. The README's DEBUG Mode Policy says what debug
mode logs.
- 2026-10-04: A site has at most one connection prompt and one signature prompt - 2026-10-04: A site has at most one connection prompt and one signature prompt
open at a time ([#405](https://git.eeqj.de/sneak/AutistMask/issues/405)). Each open at a time ([#405](https://git.eeqj.de/sneak/AutistMask/issues/405)). Each
+5 -14
View File
@@ -16,12 +16,7 @@ const {
} = require("../dustThreshold"); } = require("../dustThreshold");
const { state, saveState, currentNetwork } = require("../../shared/state"); const { state, saveState, currentNetwork } = require("../../shared/state");
const { onChainSwitch } = require("../../shared/chainSwitch"); const { onChainSwitch } = require("../../shared/chainSwitch");
const { const { log, debugFetch, setRuntimeDebug } = require("../../shared/log");
log,
debugFetch,
urlOrigin,
setRuntimeDebug,
} = require("../../shared/log");
const deleteWallet = require("./deleteWallet"); const deleteWallet = require("./deleteWallet");
const showPhrase = require("./showPhrase"); const showPhrase = require("./showPhrase");
const { walletHasRecoveryPhrase } = require("../../shared/wallet"); const { walletHasRecoveryPhrase } = require("../../shared/wallet");
@@ -277,11 +272,8 @@ function init(ctx) {
showFlash("Wrong network: expected " + net.name + "."); showFlash("Wrong network: expected " + net.name + ".");
return; return;
} }
} catch { } catch (e) {
// Not the error's message: fetch puts the whole URL, password and log.errorf("RPC validation fetch failed:", e.message);
// key included, in the message of the error it throws for a URL
// with a user name and password or one it cannot parse.
log.errorf("RPC validation fetch failed:", urlOrigin(url));
showFlash("Could not reach endpoint."); showFlash("Could not reach endpoint.");
return; return;
} }
@@ -303,9 +295,8 @@ function init(ctx) {
showFlash("Endpoint returned HTTP " + resp.status + "."); showFlash("Endpoint returned HTTP " + resp.status + ".");
return; return;
} }
} catch { } catch (e) {
// Not the error's message, as for the RPC check above. log.errorf("Blockscout validation failed:", e.message);
log.errorf("Blockscout validation failed:", urlOrigin(url));
showFlash("Could not reach endpoint."); showFlash("Could not reach endpoint.");
return; return;
} }
+1 -2
View File
@@ -43,8 +43,7 @@ const log = {
}; };
// The origin (scheme, host and port) of a URL, for logging in place of the // The origin (scheme, host and port) of a URL, for logging in place of the
// URL: RPC providers put API keys in the path or the query string, and a URL // URL: RPC providers put API keys in the path or the query string. A URL that
// can carry a user name and password, which the origin leaves out. A URL that
// does not parse gives "", so logging never stops a request. // does not parse gives "", so logging never stops a request.
function urlOrigin(url) { function urlOrigin(url) {
try { try {
+1 -10
View File
@@ -7,7 +7,7 @@
// HTTP method, the URL's origin and the JSON-RPC method, and nothing else of // HTTP method, the URL's origin and the JSON-RPC method, and nothing else of
// the request. // the request.
const { debugFetch, urlOrigin, setRuntimeDebug } = require("../src/shared/log"); const { debugFetch, setRuntimeDebug } = require("../src/shared/log");
const realFetch = globalThis.fetch; const realFetch = globalThis.fetch;
@@ -42,12 +42,3 @@ test("logs the origin and JSON-RPC method, not the key in the URL", async () =>
expect(logged).toContain("https://rpc.example.invalid"); expect(logged).toContain("https://rpc.example.invalid");
expect(logged).toContain("eth_chainId"); expect(logged).toContain("eth_chainId");
}); });
test("the origin leaves out a user name and password in the URL", () => {
expect(
urlOrigin("https://user:SECRETPASS@rpc.example.invalid/v3/KEY"),
).toBe("https://rpc.example.invalid");
expect(urlOrigin("wss://user:SECRETPASS@rpc.example.invalid:8546/")).toBe(
"wss://rpc.example.invalid:8546",
);
});
+1 -14
View File
@@ -12,11 +12,7 @@
// every log level is printed. // every log level is printed.
const { FetchRequest } = require("ethers"); const { FetchRequest } = require("ethers");
const { const { getProvider, refreshBalances } = require("../src/shared/balances");
getProvider,
lookupTokenInfo,
refreshBalances,
} = require("../src/shared/balances");
const { getFullWarnings } = require("../src/shared/addressWarnings"); const { getFullWarnings } = require("../src/shared/addressWarnings");
const { resolveEnsName } = require("../src/shared/ens"); const { resolveEnsName } = require("../src/shared/ens");
const { setRuntimeDebug } = require("../src/shared/log"); const { setRuntimeDebug } = require("../src/shared/log");
@@ -94,12 +90,3 @@ test("the balance refresh", async () => {
expectFailureLoggedWithoutKey("ETH balance failed"); expectFailureLoggedWithoutKey("ETH balance failed");
expectFailureLoggedWithoutKey("ENS reverse failed"); expectFailureLoggedWithoutKey("ENS reverse failed");
}); });
// The lookup's first line, at debug level, names the RPC endpoint; the check
// of everything printed covers it too.
test("the token lookup", async () => {
await expect(lookupTokenInfo(ADDRESS, RPC_URL, "mainnet")).rejects.toThrow(
"Not a valid ERC-20 token",
);
expectFailureLoggedWithoutKey("symbol() failed:");
});
-148
View File
@@ -1,148 +0,0 @@
// What reaches the console when an endpoint check in Settings fails.
//
// fetch refuses a URL with a user name and password in it, or one it cannot
// parse, with an error whose message carries the whole URL: the password, and
// any API key in the path or query string. The checks behind the RPC and
// Blockscout Save buttons printed that message
// (https://git.eeqj.de/sneak/AutistMask/issues/410); they now name the
// endpoint by its origin.
//
// The real fetch runs; it throws before making any request. Debug mode is on,
// so every log level is printed.
const SECRETS = ["SECRETPASS789", "PATHKEY123", "QUERYTOKEN456"];
const RPC_WITH_PASSWORD =
"https://user:SECRETPASS789@rpc.example.invalid/v3/PATHKEY123?token=QUERYTOKEN456";
// Port 99999 is out of range, so the URL does not parse.
const RPC_UNPARSEABLE =
"https://rpc.example.invalid:99999/v3/PATHKEY123?token=QUERYTOKEN456";
const BLOCKSCOUT_WITH_PASSWORD =
"https://user:SECRETPASS789@explorer.example.invalid/PATHKEY123/api/v2";
const SAVED_RPC = "https://saved-rpc.example.invalid";
const SAVED_BLOCKSCOUT = "https://saved-explorer.example.invalid/api/v2";
let elements;
let flashes;
let printed;
let state;
// A stand-in for one DOM node: enough of an element for init() to set
// properties on it and hang listeners off it.
function fakeElement() {
return {
value: "",
checked: false,
textContent: "",
href: "",
style: {},
dataset: {},
classList: { add() {}, remove() {} },
listeners: {},
addEventListener(event, handler) {
this.listeners[event] = handler;
},
querySelectorAll: () => [],
};
}
function element(id) {
return (elements[id] ||= fakeElement());
}
function loadSettingsView() {
elements = {};
flashes = [];
jest.resetModules();
jest.doMock("../src/popup/views/helpers", () => ({
$: element,
showView: () => {},
updateDebugBanner: () => {},
showFlash: (msg) => flashes.push(msg),
escapeHtml: (s) => s,
flashCopyFeedback: () => {},
goBack: () => {},
pushCurrentView: () => {},
onViewLeave: () => {},
VIEWS: [],
}));
state = require("../src/shared/state").state;
state.rpcUrl = SAVED_RPC;
state.blockscoutUrl = SAVED_BLOCKSCOUT;
require("../src/shared/log").setRuntimeDebug(true);
require("../src/popup/views/settings").init({});
}
async function save(fieldId, buttonId, typed) {
element(fieldId).value = typed;
await element(buttonId).listeners.click();
}
// The check failed, nothing was saved, and nothing printed carries the
// password or the key.
function expectFailedWithoutSecrets(label) {
expect(flashes).toContain("Could not reach endpoint.");
expect(state.rpcUrl).toBe(SAVED_RPC);
expect(state.blockscoutUrl).toBe(SAVED_BLOCKSCOUT);
const line = printed.find((text) => text.includes(label));
expect(line).toBeDefined();
const all = printed.join("\n");
for (const secret of SECRETS) {
expect(all).not.toContain(secret);
}
return line;
}
beforeEach(() => {
printed = [];
for (const method of ["log", "warn", "error"]) {
jest.spyOn(console, method).mockImplementation((...args) => {
printed.push(args.map(String).join(" "));
});
}
globalThis.chrome = {
runtime: { sendMessage: () => {} },
storage: { local: { get: async () => ({}), set: async () => {} } },
};
});
afterEach(() => {
jest.dontMock("../src/popup/views/helpers");
delete globalThis.chrome;
jest.restoreAllMocks();
});
test("fetch puts the whole URL in the error it throws for such a URL", async () => {
for (const url of [RPC_WITH_PASSWORD, RPC_UNPARSEABLE]) {
const error = await fetch(url).catch((e) => e);
expect(error.message).toContain("PATHKEY123");
}
});
test("the RPC check of a URL with a user name and password", async () => {
loadSettingsView();
await save("settings-rpc", "btn-save-rpc", RPC_WITH_PASSWORD);
const line = expectFailedWithoutSecrets("RPC validation fetch failed");
expect(line).toContain("https://rpc.example.invalid");
});
test("the RPC check of a URL that does not parse", async () => {
loadSettingsView();
await save("settings-rpc", "btn-save-rpc", RPC_UNPARSEABLE);
expectFailedWithoutSecrets("RPC validation fetch failed");
});
test("the Blockscout check of a URL with a user name and password", async () => {
loadSettingsView();
await save(
"settings-blockscout",
"btn-save-blockscout",
BLOCKSCOUT_WITH_PASSWORD,
);
const line = expectFailedWithoutSecrets("Blockscout validation failed");
expect(line).toContain("https://explorer.example.invalid");
});