Skip to content

Commit 2663378

Browse files
vveerrggclaude
andcommitted
feat: add cross-origin frame protection and Safari permission guidance
Block window.nostr injection in cross-origin iframes by default (defense-in-depth). Add toggle in Settings > Security to control the behavior. Add Safari "Always Allow" guidance to the support page so users understand per-site permission prompts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c679de3 commit 2663378

16 files changed

Lines changed: 331 additions & 30 deletions

distros/chrome/background-sw.build.js

Lines changed: 13 additions & 2 deletions
Large diffs are not rendered by default.

distros/chrome/background.build.js

Lines changed: 13 additions & 2 deletions
Large diffs are not rendered by default.

distros/chrome/content.build.js

Lines changed: 22 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

distros/chrome/sidepanel.build.js

Lines changed: 48 additions & 1 deletion
Large diffs are not rendered by default.

distros/chrome/sidepanel.html

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,11 @@
233233
</div>
234234
</div>
235235
<div style="flex-shrink:0;padding:12px 16px;border-top:1px solid #49483e;background:#3e3d32;text-align:center;font-size:0.75rem;">
236-
<a href="https://nostrkey.com" target="_blank" rel="noopener" style="color:#b0b0a8;text-decoration:none;">NostrKey.com</a>
236+
<a id="footer-home-link" href="https://nostrkey.com" style="color:#b0b0a8;text-decoration:none;cursor:pointer;">NostrKey.com</a>
237237
<span style="color:#49483e;margin:0 6px;">|</span>
238-
<a href="https://nostrkey.com/terms.html" target="_blank" rel="noopener" style="color:#b0b0a8;text-decoration:none;">Ts &amp; Cs</a>
238+
<a id="footer-terms-link" href="https://nostrkey.com/terms.html" style="color:#b0b0a8;text-decoration:none;cursor:pointer;">Ts &amp; Cs</a>
239239
<span style="color:#49483e;margin:0 6px;">|</span>
240-
<a href="https://nostrkey.com/support.html#donate" target="_blank" rel="noopener" style="color:#a6e22e;text-decoration:none;">Donate</a>
240+
<a id="footer-donate-link" href="https://nostrkey.com/support.html#donate" style="color:#a6e22e;text-decoration:none;cursor:pointer;">Donate</a>
241241
</div>
242242
</div>
243243

@@ -510,6 +510,12 @@ <h2 class="section-title">API Keys</h2>
510510
<h2 class="section-title">Security</h2>
511511
<button id="settings-security-btn" class="button w-full mb-2">Master Password</button>
512512
<button id="settings-autolock-btn" class="button w-full">Auto-lock Settings</button>
513+
<label class="flex items-center gap-3 cursor-pointer" style="padding:4px 0;margin-top:8px;">
514+
<input id="frame-protection-toggle" type="checkbox"
515+
style="width:18px;height:18px;accent-color:#a6e22e;" />
516+
<span style="color:#f8f8f2;font-size:14px;">Block cross-origin frames</span>
517+
</label>
518+
<p id="frame-protection-status" style="color:#b0b0a8;font-size:0.8rem;margin-top:4px;"></p>
513519
</div>
514520
<div class="section-card">
515521
<h2 class="section-title">Sync</h2>

distros/safari/background.build.js

Lines changed: 13 additions & 2 deletions
Large diffs are not rendered by default.

distros/safari/background.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,12 @@ let encryptionEnabled = false; // cached encryption state for fast lookups
102102
let autoLockTimeout = 15 * 60 * 1000; // 15 minutes default
103103
let autoLockTimer = null;
104104
let nostrAccessWhileLocked = false;
105+
let blockCrossOriginFrames = true;
105106

106107
// Load persisted state on startup
107108
(async () => {
108109
log('[STARTUP] Reading persisted state...');
109-
const data = await storage.get({ autoLockMinutes: 15, isEncrypted: false, passwordHash: null, nostrAccessWhileLocked: false });
110+
const data = await storage.get({ autoLockMinutes: 15, isEncrypted: false, passwordHash: null, nostrAccessWhileLocked: false, blockCrossOriginFrames: true });
110111
log(`[STARTUP] isEncrypted=${data.isEncrypted}, passwordHash=${data.passwordHash ? 'EXISTS' : 'null'}, autoLockMinutes=${data.autoLockMinutes}`);
111112
autoLockTimeout = data.autoLockMinutes * 60 * 1000;
112113
// Defensive: if passwordHash exists but flag is stale, self-heal
@@ -117,6 +118,7 @@ let nostrAccessWhileLocked = false;
117118
}
118119
encryptionEnabled = data.isEncrypted;
119120
nostrAccessWhileLocked = !!data.nostrAccessWhileLocked;
121+
blockCrossOriginFrames = data.blockCrossOriginFrames !== false;
120122
// If encryption is enabled, we start locked
121123
locked = encryptionEnabled;
122124
log(`[STARTUP] Final state: encryptionEnabled=${encryptionEnabled}, locked=${locked}`);
@@ -415,6 +417,7 @@ api.runtime.onMessage.addListener((message, _sender, sendResponse) => {
415417
locked = false;
416418
encryptionEnabled = false;
417419
nostrAccessWhileLocked = false;
420+
blockCrossOriginFrames = true;
418421
// Re-initialize with default profile
419422
await storage.set({
420423
profiles: [{ name: 'Default Nostr Profile', privKey: '', pubKey: '' }],
@@ -459,6 +462,14 @@ api.runtime.onMessage.addListener((message, _sender, sendResponse) => {
459462
}
460463
sendResponse(true);
461464
return true;
465+
case 'getBlockCrossOriginFrames':
466+
sendResponse(blockCrossOriginFrames);
467+
return true;
468+
case 'setBlockCrossOriginFrames':
469+
blockCrossOriginFrames = !!message.payload;
470+
storage.set({ blockCrossOriginFrames: !!message.payload });
471+
sendResponse(true);
472+
return true;
462473
case 'getActiveProfileInfo':
463474
(async () => {
464475
try {

distros/safari/content.build.js

Lines changed: 22 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

distros/safari/content.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,27 @@
11
import { api } from './utilities/browser-polyfill';
22

3-
let script = document.createElement('script');
4-
script.setAttribute('src', api.runtime.getURL('nostr.build.js'));
5-
document.body.appendChild(script);
3+
async function shouldInject() {
4+
if (window === window.top) return true;
5+
try {
6+
const data = await api.storage.local.get({ blockCrossOriginFrames: true });
7+
if (!data.blockCrossOriginFrames) return true;
8+
} catch {
9+
return false;
10+
}
11+
try {
12+
void window.top.location.href; // throws for cross-origin frames
13+
return true;
14+
} catch {
15+
return false;
16+
}
17+
}
18+
19+
shouldInject().then(inject => {
20+
if (!inject) return;
21+
let script = document.createElement('script');
22+
script.setAttribute('src', api.runtime.getURL('nostr.build.js'));
23+
document.body.appendChild(script);
24+
});
625

726
// Permission bottom sheet
827
let permissionSheet = null;

distros/safari/sidepanel.build.js

Lines changed: 48 additions & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)