harden: drop 'unsafe-inline' from style-src (closes #328)
check / check (push) Failing after 3s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s

The popup's markup no longer carries style attributes. The 42 in
index.html and in the HTML the view helpers build are now Tailwind
classes, each computing to the value it replaced, so style-src is 'self'
in both manifests, pinned in tests/manifest.test.js.

The address dot's 16 colours are written out as whole classes, because
Tailwind builds only the classes it finds in the source. The Settings
debug well is shown and hidden with the hidden class, since clearing an
inline display no longer uncovers it. Two tests that found the colour dot
by its inline style now find it by its class. Script that sets
element.style is unaffected.

Model: opus-5-5
This commit is contained in:
2026-10-05 12:14:42 +00:00
parent 9bd607b411
commit 6c005b7893
13 changed files with 108 additions and 147 deletions
+32 -83
View File
@@ -110,8 +110,7 @@
</div>
<div
id="add-wallet-phrase-warning"
class="text-xs mb-2 border border-border border-dashed p-2"
style="visibility: hidden"
class="text-xs mb-2 border border-border border-dashed p-2 invisible"
>
Write these words down and keep them safe. Anyone with
them can take your funds; if you lose them, your wallet
@@ -262,10 +261,7 @@
<!-- recent transactions across all addresses -->
<div>
<div
class="font-bold bg-section py-1 px-2"
style="margin-left: -0.5rem; margin-right: -0.5rem"
>
<div class="font-bold bg-section py-1 px-2 -mx-2">
Recent Transactions
</div>
<div id="home-tx-list">
@@ -273,7 +269,7 @@
</div>
</div>
<div class="py-1" style="margin: 0 -0.5rem">&nbsp;</div>
<div class="py-1 -mx-2">&nbsp;</div>
<div class="text-xs text-muted">
<span
@@ -409,8 +405,7 @@
</p>
<div
id="export-privkey-flash"
class="text-xs mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs mb-2 min-h-[1.25rem] invisible"
></div>
<div id="export-privkey-password-section" class="mb-2">
<label class="block mb-1">Password</label>
@@ -542,8 +537,7 @@
/>
<div
id="send-to-error"
class="text-xs"
style="min-height: 1.25rem; color: #cc0000"
class="text-xs min-h-[1.25rem] text-[#cc0000]"
></div>
</div>
<div class="mb-2">
@@ -619,7 +613,7 @@
<div class="text-xs text-muted mb-1">Your balance</div>
<div id="confirm-balance" class="text-xs"></div>
</div>
<div id="confirm-fee" class="mb-3" style="visibility: hidden">
<div id="confirm-fee" class="mb-3 invisible">
<div class="text-xs text-muted mb-1">Network fee</div>
<div id="confirm-fee-amount" class="text-xs"></div>
<!-- Holds its one line of space from the first paint, so
@@ -627,22 +621,13 @@
nothing. The placeholder is never seen. -->
<div
id="confirm-fee-reserve"
class="text-xs text-muted"
style="visibility: hidden"
class="text-xs text-muted invisible"
>
reserve pending
</div>
</div>
<div
id="confirm-warnings"
class="mb-2"
style="visibility: hidden"
></div>
<div
id="confirm-recipient-warning"
class="mb-2"
style="visibility: hidden"
>
<div id="confirm-warnings" class="mb-2 invisible"></div>
<div id="confirm-recipient-warning" class="mb-2 invisible">
<div
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
>
@@ -655,14 +640,9 @@
in confirmTx.js sets it. -->
<div
id="confirm-contract-warning"
class="mb-2 border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
style="visibility: hidden"
class="mb-2 border border-red-500 border-dashed p-2 text-xs font-bold text-red-500 invisible"
></div>
<div
id="confirm-burn-warning"
class="mb-2"
style="visibility: hidden"
>
<div id="confirm-burn-warning" class="mb-2 invisible">
<div
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
>
@@ -670,11 +650,7 @@
here are permanently destroyed and cannot be recovered.
</div>
</div>
<div
id="confirm-etherscan-warning"
class="mb-2"
style="visibility: hidden"
>
<div id="confirm-etherscan-warning" class="mb-2 invisible">
<div
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
>
@@ -684,13 +660,11 @@
</div>
<div
id="confirm-errors"
class="mb-2 border border-border border-dashed p-2"
style="visibility: hidden; min-height: 1.25rem"
class="mb-2 border border-border border-dashed p-2 invisible min-h-[1.25rem]"
></div>
<div
id="confirm-amount-fee-error"
class="mb-2 border border-border border-dashed p-2 text-xs"
style="visibility: hidden"
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
>
Your balance does not cover this amount plus the network
fee. Please go back and send a smaller amount.
@@ -699,15 +673,13 @@
in confirmTx.js sets it. -->
<div
id="confirm-gas-error"
class="mb-2 border border-border border-dashed p-2 text-xs"
style="visibility: hidden"
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
></div>
<!-- Its sentence names why the fee could not be estimated,
so show() in confirmTx.js sets it. -->
<div
id="confirm-fee-unknown-error"
class="mb-2 border border-border border-dashed p-2 text-xs"
style="visibility: hidden"
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
></div>
<div class="mb-2">
<label class="block mb-1 text-xs">Password</label>
@@ -719,8 +691,7 @@
</div>
<div
id="confirm-tx-password-error"
class="text-xs mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs mb-2 min-h-[1.25rem] invisible"
></div>
<button
id="btn-confirm-send"
@@ -835,8 +806,7 @@
</button>
<div
id="receive-erc20-warning"
class="text-xs border border-border border-dashed p-2 mt-3"
style="visibility: hidden"
class="text-xs border border-border border-dashed p-2 mt-3 invisible"
></div>
</div>
@@ -864,8 +834,7 @@
</div>
<div
id="add-token-info"
class="text-xs text-muted mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-muted mb-2 min-h-[1.25rem] invisible"
></div>
<div class="mb-2">
<label class="block mb-1 text-xs text-muted"
@@ -1051,8 +1020,7 @@
type="text"
inputmode="numeric"
id="settings-dust-threshold"
class="border border-border p-1 text-xs bg-bg text-fg"
style="width: 10ch"
class="border border-border p-1 text-xs bg-bg text-fg w-[10ch]"
/>
<span class="text-xs text-muted">gwei</span>
</div>
@@ -1129,8 +1097,7 @@
<div
id="settings-debug-well"
class="bg-well p-3 mx-1 mb-3"
style="display: none"
class="bg-well p-3 mx-1 mb-3 hidden"
>
<h3 class="font-bold mb-1">Debug</h3>
<label
@@ -1158,8 +1125,7 @@
</p>
<div
id="delete-wallet-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
></div>
<div class="mb-2">
<label class="block mb-1">Password</label>
@@ -1232,8 +1198,7 @@
</div>
<div
id="delete-wallet-lost-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
></div>
<button
id="btn-delete-wallet-lost-confirm"
@@ -1288,8 +1253,7 @@
</p>
<div
id="delete-address-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
></div>
<button
id="btn-delete-address-confirm"
@@ -1318,8 +1282,7 @@
</div>
<div
id="show-phrase-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
></div>
<div id="show-phrase-password-section" class="mb-2">
<label class="block mb-1">Password</label>
@@ -1401,8 +1364,7 @@
/>
<div
id="settings-addtoken-info"
class="text-xs text-muted mt-1 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-muted mt-1 min-h-[1.25rem] invisible"
></div>
<button
id="btn-settings-addtoken-manual"
@@ -1635,8 +1597,7 @@
</div>
<div
id="approve-tx-error"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem]"
style="visibility: hidden"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem] invisible"
></div>
<div class="flex justify-between">
<button
@@ -1672,15 +1633,7 @@
<div
id="approve-sign-danger-warning"
class="mb-3 p-2 text-xs font-bold"
style="
visibility: hidden;
min-height: 1.25rem;
background: #fee2e2;
color: #991b1b;
border: 2px solid #dc2626;
border-radius: 6px;
"
class="mb-3 p-2 text-xs font-bold invisible min-h-[1.25rem] bg-[#fee2e2] text-[#991b1b] border-2 border-[#dc2626] rounded-[6px]"
></div>
<div class="mb-3">
@@ -1697,8 +1650,7 @@
<div class="text-xs text-muted mb-1">Message</div>
<div
id="approve-sign-message"
class="text-xs break-all"
style="max-height: 12rem; overflow-y: auto"
class="text-xs break-all max-h-48 overflow-y-auto"
></div>
</div>
@@ -1706,8 +1658,7 @@
<div class="text-xs text-muted mb-1">Raw data</div>
<div
id="approve-sign-hex"
class="text-xs break-all"
style="max-height: 6rem; overflow-y: auto"
class="text-xs break-all max-h-24 overflow-y-auto"
></div>
</div>
@@ -1721,8 +1672,7 @@
</div>
<div
id="approve-sign-error"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem]"
style="visibility: hidden"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem] invisible"
></div>
<div class="flex justify-between">
<button
@@ -1846,8 +1796,7 @@
</div>
<div
id="state-recovery-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
></div>
<button
id="btn-state-recovery-reset"
+2 -2
View File
@@ -181,10 +181,10 @@ function renderTransactions(txs) {
// it on the line above rather than replacing it.
const nameStr = escapeHtml(title || ensName || "");
const err = tx.isError ? " (failed)" : "";
const opacity = tx.isError ? " opacity:0.5;" : "";
const opacity = tx.isError ? " opacity-50" : "";
const ago = escapeHtml(timeAgo(tx.timestamp));
const iso = escapeHtml(isoDate(tx.timestamp));
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
html += txCounterpartyHtml(counterparty, nameStr, amountStr);
html += `</div>`;
+2 -2
View File
@@ -258,10 +258,10 @@ function renderTransactions(txs) {
// it on the line above rather than replacing it.
const nameStr = escapeHtml(title || ensName || "");
const err = tx.isError ? " (failed)" : "";
const opacity = tx.isError ? " opacity:0.5;" : "";
const opacity = tx.isError ? " opacity-50" : "";
const ago = escapeHtml(timeAgo(tx.timestamp));
const iso = escapeHtml(isoDate(tx.timestamp));
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
html += txCounterpartyHtml(counterparty, nameStr, amountStr);
html += `</div>`;
+23 -20
View File
@@ -335,7 +335,7 @@ function balanceLine(symbol, amount, price, tokenId) {
: "";
return (
`<div class="flex text-xs${clickClass}"${tokenAttr}>` +
`<span class="flex justify-between" style="width:42ch;max-width:100%">` +
`<span class="flex justify-between w-[42ch] max-w-full">` +
`<span>${escapeHtml(displaySymbol(symbol))}</span>` +
`<span>${qty}</span>` +
`</span>` +
@@ -430,23 +430,26 @@ function truncateMiddle(str, maxLen) {
// 16 colors evenly spaced around the hue wheel (22.5° apart),
// all at HSL saturation 70%, lightness 50% for uniform vibrancy.
// Each is a whole Tailwind class: Tailwind builds only the classes it finds
// written out in the source, so the class name cannot be put together at
// runtime.
const ADDRESS_COLORS = [
"#d92626",
"#d96926",
"#d9ac26",
"#c2d926",
"#80d926",
"#3dd926",
"#26d953",
"#26d996",
"#26d9d9",
"#2696d9",
"#2653d9",
"#3d26d9",
"#8026d9",
"#c226d9",
"#d926ac",
"#d92669",
"bg-[#d92626]",
"bg-[#d96926]",
"bg-[#d9ac26]",
"bg-[#c2d926]",
"bg-[#80d926]",
"bg-[#3dd926]",
"bg-[#26d953]",
"bg-[#26d996]",
"bg-[#26d9d9]",
"bg-[#2696d9]",
"bg-[#2653d9]",
"bg-[#3d26d9]",
"bg-[#8026d9]",
"bg-[#c226d9]",
"bg-[#d926ac]",
"bg-[#d92669]",
];
function addressColor(address) {
@@ -456,12 +459,12 @@ function addressColor(address) {
function addressDotHtml(address) {
const color = addressColor(address);
return `<span style="width:8px;height:8px;border-radius:50%;display:inline-block;background:${color};margin-right:4px;vertical-align:middle;flex-shrink:0;"></span>`;
return `<span class="inline-block w-[8px] h-[8px] rounded-[50%] ${color} mr-[4px] align-middle shrink-0"></span>`;
}
function blockieHtml(address) {
const src = makeBlockie(address);
return `<img src="${escapeHtml(src)}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
return `<img src="${escapeHtml(src)}" width="48" height="48" class="inline-block rounded-[50%] [image-rendering:pixelated]">`;
}
// Look up an address across all wallets and return its title
@@ -571,7 +574,7 @@ function timeAgo(timestamp) {
// Shared external-link icon SVG used across all views.
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<span class="inline-block w-[10px] h-[10px] ml-[4px] align-middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
+5 -5
View File
@@ -131,10 +131,10 @@ function renderHomeTxList(ctx) {
const title = addressTitle(counterparty, state.wallets);
const titleStr = title ? escapeHtml(title) : "";
const err = tx.isError ? " (failed)" : "";
const opacity = tx.isError ? " opacity:0.5;" : "";
const opacity = tx.isError ? " opacity-50" : "";
const ago = escapeHtml(timeAgo(tx.timestamp));
const iso = escapeHtml(isoDate(tx.timestamp));
html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
html += txCounterpartyHtml(counterparty, titleStr, amountStr);
html += `</div>`;
@@ -241,7 +241,7 @@ function walletListHtml() {
state.wallets.forEach((wallet, wi) => {
const defect = walletDefect(wallet);
html += `<div>`;
html += `<div class="flex justify-between items-center bg-section py-1 px-2" style="margin:0 -0.5rem">`;
html += `<div class="flex justify-between items-center bg-section py-1 px-2 -mx-2">`;
html += `<span class="font-bold cursor-pointer wallet-name underline decoration-dashed" data-wallet="${wi}">${escapeHtml(wallet.name)}</span>`;
// No "+" on a defective wallet: deriving another address from that
// xpub would only add one more address the key does not produce
@@ -255,12 +255,12 @@ function walletListHtml() {
wallet.addresses.forEach((addr, ai) => {
html += `<div class="address-row py-1 border-b border-border-light cursor-pointer hover:bg-hover" data-wallet="${wi}" data-address="${ai}">`;
const isActive = state.activeAddress === addr.address;
const infoBtn = `<span class="btn-addr-info text-xs cursor-pointer border border-border hover:bg-fg hover:text-bg" style="padding:0" data-wallet="${wi}" data-address="${ai}">[info]</span>`;
const infoBtn = `<span class="btn-addr-info text-xs cursor-pointer border border-border hover:bg-fg hover:text-bg p-0" data-wallet="${wi}" data-address="${ai}">[info]</span>`;
// Only where a wallet can spare the address: a wallet holding a
// single address has no remove control, because its last address
// is never removable.
const removeBtn = canRemoveAddress(wallet)
? `<span class="btn-remove-address text-xs cursor-pointer border border-border hover:bg-fg hover:text-bg ml-1" style="padding:0" data-wallet="${wi}" data-address="${ai}" title="Remove this address from the wallet">[x]</span>`
? `<span class="btn-remove-address text-xs cursor-pointer border border-border hover:bg-fg hover:text-bg ml-1 p-0" data-wallet="${wi}" data-address="${ai}" title="Remove this address from the wallet">[x]</span>`
: "";
const dot = addressDotHtml(addr.address);
const titleBold = isActive ? "font-bold" : "";
+2 -7
View File
@@ -213,12 +213,7 @@ function show() {
versionClickCount = 0;
// Show debug well if debug mode is already enabled
const debugWell = $("settings-debug-well");
if (state.debugMode) {
debugWell.style.display = "";
} else {
debugWell.style.display = "none";
}
$("settings-debug-well").classList.toggle("hidden", !state.debugMode);
$("settings-debug-mode").checked = state.debugMode;
showView("settings");
@@ -434,7 +429,7 @@ function init(ctx) {
if (versionClickCount >= 10) {
versionClickCount = 0;
clearTimeout(versionClickTimer);
$("settings-debug-well").style.display = "";
$("settings-debug-well").classList.remove("hidden");
}
});