harden: debug mode logs only a request's origin and JSON-RPC method (closes #410)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 3s
e2e / e2e-firefox (push) Failing after 3s

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 README's DEBUG Mode Policy says what debug mode logs.

Model: opus-5-5
This commit is contained in:
2026-10-04 17:50:47 +00:00
parent d1751beb32
commit b261bafb03
7 changed files with 91 additions and 8 deletions
+10
View File
@@ -2168,6 +2168,16 @@ the log level and turns the banner on, and that is all it may ever do: it feeds
constant directly, so no runtime toggle in a release build can reach the constant directly, so no runtime toggle in a release build can reach the
hardcoded test phrase. hardcoded test phrase.
At the raised log level the console also shows the wallet's addresses with their
balances and ENS names, the token contracts looked up, and a line for each
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
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
refresh and token lookup name the RPC endpoint by its origin too. The URL's path
and query string, where RPC providers put API keys, and the request body are
never logged.
### Key Decisions ### Key Decisions
- **No framework**: The popup UI is vanilla JS and HTML. The extension is small - **No framework**: The popup UI is vanilla JS and HTML. The extension is small
+8
View File
@@ -45,6 +45,14 @@ but the review is broader than any of them.
# Completed Steps # Completed Steps
- 2026-10-04: Debug mode no longer writes RPC API keys to the console
([#410](https://git.eeqj.de/sneak/AutistMask/issues/410)). `debugFetch` logged
every request's full URL and body, so an RPC endpoint with a key in its path
or query string printed that key on every request. It now logs the HTTP
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. 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
`eth_requestAccounts` or `personal_sign` call opened another approval window, `eth_requestAccounts` or `personal_sign` call opened another approval window,
+3 -3
View File
@@ -10,7 +10,7 @@ const {
} = require("ethers"); } = require("ethers");
const { ERC20_ABI } = require("./constants"); const { ERC20_ABI } = require("./constants");
const { NETWORKS } = require("./networks"); const { NETWORKS } = require("./networks");
const { log, debugFetch } = require("./log"); const { log, debugFetch, urlOrigin } = require("./log");
const { deriveAddressFromXpub } = require("./wallet"); const { deriveAddressFromXpub } = require("./wallet");
const { TOKEN_BY_ADDRESS } = require("./tokenList"); const { TOKEN_BY_ADDRESS } = require("./tokenList");
const { LOW_HOLDER_THRESHOLD, parseHoldersCount } = require("./holders"); const { LOW_HOLDER_THRESHOLD, parseHoldersCount } = require("./holders");
@@ -203,7 +203,7 @@ async function refreshBalances(
trackedTokens, trackedTokens,
networkId, networkId,
) { ) {
log.debugf("refreshBalances start, rpc:", rpcUrl); log.debugf("refreshBalances start, rpc:", urlOrigin(rpcUrl));
const provider = getProvider(rpcUrl, networkId); const provider = getProvider(rpcUrl, networkId);
const updates = []; const updates = [];
@@ -280,7 +280,7 @@ async function refreshBalances(
// Look up token metadata from its contract. // Look up token metadata from its contract.
// Calls symbol() and decimals() to verify it implements ERC-20. // Calls symbol() and decimals() to verify it implements ERC-20.
async function lookupTokenInfo(contractAddress, rpcUrl, networkId) { async function lookupTokenInfo(contractAddress, rpcUrl, networkId) {
log.debugf("lookupTokenInfo", contractAddress, "rpc:", rpcUrl); log.debugf("lookupTokenInfo", contractAddress, "rpc:", urlOrigin(rpcUrl));
const provider = getProvider(rpcUrl, networkId); const provider = getProvider(rpcUrl, networkId);
const contract = new Contract(contractAddress, ERC20_ABI, provider); const contract = new Contract(contractAddress, ERC20_ABI, provider);
+24 -5
View File
@@ -42,14 +42,33 @@ const log = {
}, },
}; };
// Fetch wrapper that debug-logs every request and response. // 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. A URL that
// does not parse gives "", so logging never stops a request.
function urlOrigin(url) {
try {
return new URL(url).origin;
} catch {
return "";
}
}
// Fetch wrapper that debug-logs every request and response. It logs the
// URL's origin and, for a JSON-RPC body, the method name: never the full URL
// or body, which can carry an API key or a signed transaction.
async function debugFetch(url, opts) { async function debugFetch(url, opts) {
const method = (opts && opts.method) || "GET"; const method = (opts && opts.method) || "GET";
const body = opts && opts.body; const origin = urlOrigin(url);
log.debugf("fetch →", method, url, body || ""); let rpcMethod = "";
try {
rpcMethod = JSON.parse(opts.body).method || "";
} catch {
// no body, or a body that is not JSON
}
log.debugf("fetch →", method, origin, rpcMethod);
const resp = await fetch(url, opts); const resp = await fetch(url, opts);
log.debugf("fetch ←", resp.status, url); log.debugf("fetch ←", resp.status, origin);
return resp; return resp;
} }
module.exports = { log, debugFetch, setRuntimeDebug, isDebug }; module.exports = { log, debugFetch, urlOrigin, setRuntimeDebug, isDebug };
+44
View File
@@ -0,0 +1,44 @@
// What debugFetch writes to the console in debug mode.
//
// RPC providers put the API key in the URL's path or query string, and the
// debug log used to print the whole URL and request body, so turning debug
// mode on wrote the key to the console
// (https://git.eeqj.de/sneak/AutistMask/issues/410). The log now names the
// HTTP method, the URL's origin and the JSON-RPC method, and nothing else of
// the request.
const { debugFetch, setRuntimeDebug } = require("../src/shared/log");
const realFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = realFetch;
setRuntimeDebug(false);
jest.restoreAllMocks();
});
test("logs the origin and JSON-RPC method, not the key in the URL", async () => {
setRuntimeDebug(true);
const consoleLog = jest.spyOn(console, "log").mockImplementation(() => {});
globalThis.fetch = jest.fn(async () => ({ status: 200 }));
await debugFetch(
"https://rpc.example.invalid/v3/PATHKEY123?token=QUERYTOKEN456",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "eth_chainId",
params: [],
}),
},
);
const logged = consoleLog.mock.calls.flat().join(" ");
expect(logged).not.toContain("PATHKEY123");
expect(logged).not.toContain("QUERYTOKEN456");
expect(logged).toContain("https://rpc.example.invalid");
expect(logged).toContain("eth_chainId");
});
+1
View File
@@ -66,6 +66,7 @@ jest.mock("../src/shared/log", () => ({
status: 200, status: 200,
json: async () => mockExplorer.items, json: async () => mockExplorer.items,
})), })),
urlOrigin: () => "",
setRuntimeDebug: () => {}, setRuntimeDebug: () => {},
isDebug: () => false, isDebug: () => false,
})); }));
+1
View File
@@ -44,6 +44,7 @@ jest.mock("../src/shared/log", () => ({
errorf: () => {}, errorf: () => {},
}, },
debugFetch: jest.fn(), debugFetch: jest.fn(),
urlOrigin: () => "",
setRuntimeDebug: () => {}, setRuntimeDebug: () => {},
isDebug: () => false, isDebug: () => false,
})); }));