Skip to content

Commit 7c6291f

Browse files
committed
security: fix auto-lock bypass, validate settings, gate sensitive messages
C1: Remove resetAutoLock() from ask() and getNpub — a malicious page could poll getPublicKey() to prevent the timer from ever firing. Timer now only resets on genuine user actions (approve, save, export). M2: Validate autoLockMinutes against allowed values [0,5,15,30,60,90,180]. Rejects NaN, Infinity, negative, or arbitrary numbers. H2: Stop logging plaintext private key prefixes in hasEncryptedData. H3: Add sender validation — sensitive operations (setPassword, removePassword, resetAllData, changePassword, setAutoLockTimeout, setNostrAccessWhileLocked, setBlockCrossOriginFrames, backup.*, unlock) now reject messages from content script contexts. Only extension pages (popup, sidepanel, options) are allowed.
1 parent f616dbd commit 7c6291f

3 files changed

Lines changed: 106 additions & 19 deletions

File tree

distros/safari/background.build.js

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

distros/safari/background.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -281,10 +281,35 @@ async function checkLockState() {
281281
return locked;
282282
}
283283

284+
// --- Sender validation -------------------------------------------------------
285+
286+
const SENSITIVE_KINDS = new Set([
287+
'setPassword', 'changePassword', 'removePassword', 'resetAllData',
288+
'setAutoLockTimeout', 'setNostrAccessWhileLocked', 'setBlockCrossOriginFrames',
289+
'backup.export', 'backup.import', 'unlock',
290+
]);
291+
292+
function isExtensionSender(sender) {
293+
// Messages from extension pages (popup, sidepanel, options) have our ID
294+
// and a URL starting with our extension origin. Content scripts have a
295+
// tab property — they are page context and must not access sensitive ops.
296+
if (sender.id !== api.runtime.id) return false;
297+
if (sender.tab) return false; // content script context
298+
return true;
299+
}
300+
284301
// --- Message handler --------------------------------------------------------
285302

286303
api.runtime.onMessage.addListener((message, _sender, sendResponse) => {
287304
log(message);
305+
306+
// Block sensitive operations from non-extension contexts
307+
if (SENSITIVE_KINDS.has(message.kind) && !isExtensionSender(_sender)) {
308+
log(`[SECURITY] Blocked ${message.kind} from non-extension sender`);
309+
sendResponse({ success: false, error: 'Unauthorized sender' });
310+
return true;
311+
}
312+
288313
let uuid = crypto.randomUUID();
289314
let sr;
290315

@@ -318,7 +343,6 @@ api.runtime.onMessage.addListener((message, _sender, sendResponse) => {
318343
resetAutoLock();
319344
return savePrivateKey(message.payload);
320345
case 'getNpub':
321-
resetAutoLock();
322346
(async () => {
323347
try {
324348
const result = await getNpub(message.payload);
@@ -404,7 +428,7 @@ api.runtime.onMessage.addListener((message, _sender, sendResponse) => {
404428
for (let i = 0; i < data.profiles.length; i++) {
405429
const p = data.profiles[i];
406430
const isEnc = p.privKey ? isEncryptedBlob(p.privKey) : false;
407-
log(`[hasEncryptedData] profile[${i}] name="${p.name}" privKey=${p.privKey ? (isEnc ? 'ENCRYPTED' : 'PLAINTEXT(' + p.privKey.substring(0, 8) + '...)') : 'EMPTY'}`);
431+
log(`[hasEncryptedData] profile[${i}] name="${p.name}" privKey=${p.privKey ? (isEnc ? 'ENCRYPTED' : 'PLAINTEXT') : 'EMPTY'}`);
408432
if (isEnc) encryptedProfiles++;
409433
}
410434
}
@@ -509,12 +533,19 @@ api.runtime.onMessage.addListener((message, _sender, sendResponse) => {
509533
}
510534
})();
511535
return true;
512-
case 'setAutoLockTimeout':
513-
autoLockTimeout = message.payload * 60 * 1000; // payload in minutes
514-
storage.set({ autoLockMinutes: message.payload });
536+
case 'setAutoLockTimeout': {
537+
const ALLOWED_LOCK_MINUTES = [0, 5, 15, 30, 60, 90, 180];
538+
const mins = Number(message.payload);
539+
if (!ALLOWED_LOCK_MINUTES.includes(mins)) {
540+
sendResponse(false);
541+
return true;
542+
}
543+
autoLockTimeout = mins * 60 * 1000;
544+
storage.set({ autoLockMinutes: mins });
515545
resetAutoLock();
516546
sendResponse(true);
517547
return true;
548+
}
518549
case 'getAutoLockTimeout':
519550
reply(sendResponse, async () => {
520551
const { autoLockMinutes } = await storage.get({ autoLockMinutes: 15 });
@@ -1220,7 +1251,6 @@ async function ask(uuid, { kind, host, payload }) {
12201251
return;
12211252
}
12221253

1223-
resetAutoLock();
12241254
await forceRelease(); // Clean up previous tab if it closed without cleaning itself up
12251255
prompt.release = await prompt.mutex.acquire();
12261256

src/background.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -281,10 +281,35 @@ async function checkLockState() {
281281
return locked;
282282
}
283283

284+
// --- Sender validation -------------------------------------------------------
285+
286+
const SENSITIVE_KINDS = new Set([
287+
'setPassword', 'changePassword', 'removePassword', 'resetAllData',
288+
'setAutoLockTimeout', 'setNostrAccessWhileLocked', 'setBlockCrossOriginFrames',
289+
'backup.export', 'backup.import', 'unlock',
290+
]);
291+
292+
function isExtensionSender(sender) {
293+
// Messages from extension pages (popup, sidepanel, options) have our ID
294+
// and a URL starting with our extension origin. Content scripts have a
295+
// tab property — they are page context and must not access sensitive ops.
296+
if (sender.id !== api.runtime.id) return false;
297+
if (sender.tab) return false; // content script context
298+
return true;
299+
}
300+
284301
// --- Message handler --------------------------------------------------------
285302

286303
api.runtime.onMessage.addListener((message, _sender, sendResponse) => {
287304
log(message);
305+
306+
// Block sensitive operations from non-extension contexts
307+
if (SENSITIVE_KINDS.has(message.kind) && !isExtensionSender(_sender)) {
308+
log(`[SECURITY] Blocked ${message.kind} from non-extension sender`);
309+
sendResponse({ success: false, error: 'Unauthorized sender' });
310+
return true;
311+
}
312+
288313
let uuid = crypto.randomUUID();
289314
let sr;
290315

@@ -318,7 +343,6 @@ api.runtime.onMessage.addListener((message, _sender, sendResponse) => {
318343
resetAutoLock();
319344
return savePrivateKey(message.payload);
320345
case 'getNpub':
321-
resetAutoLock();
322346
(async () => {
323347
try {
324348
const result = await getNpub(message.payload);
@@ -404,7 +428,7 @@ api.runtime.onMessage.addListener((message, _sender, sendResponse) => {
404428
for (let i = 0; i < data.profiles.length; i++) {
405429
const p = data.profiles[i];
406430
const isEnc = p.privKey ? isEncryptedBlob(p.privKey) : false;
407-
log(`[hasEncryptedData] profile[${i}] name="${p.name}" privKey=${p.privKey ? (isEnc ? 'ENCRYPTED' : 'PLAINTEXT(' + p.privKey.substring(0, 8) + '...)') : 'EMPTY'}`);
431+
log(`[hasEncryptedData] profile[${i}] name="${p.name}" privKey=${p.privKey ? (isEnc ? 'ENCRYPTED' : 'PLAINTEXT') : 'EMPTY'}`);
408432
if (isEnc) encryptedProfiles++;
409433
}
410434
}
@@ -509,12 +533,19 @@ api.runtime.onMessage.addListener((message, _sender, sendResponse) => {
509533
}
510534
})();
511535
return true;
512-
case 'setAutoLockTimeout':
513-
autoLockTimeout = message.payload * 60 * 1000; // payload in minutes
514-
storage.set({ autoLockMinutes: message.payload });
536+
case 'setAutoLockTimeout': {
537+
const ALLOWED_LOCK_MINUTES = [0, 5, 15, 30, 60, 90, 180];
538+
const mins = Number(message.payload);
539+
if (!ALLOWED_LOCK_MINUTES.includes(mins)) {
540+
sendResponse(false);
541+
return true;
542+
}
543+
autoLockTimeout = mins * 60 * 1000;
544+
storage.set({ autoLockMinutes: mins });
515545
resetAutoLock();
516546
sendResponse(true);
517547
return true;
548+
}
518549
case 'getAutoLockTimeout':
519550
reply(sendResponse, async () => {
520551
const { autoLockMinutes } = await storage.get({ autoLockMinutes: 15 });
@@ -1220,7 +1251,6 @@ async function ask(uuid, { kind, host, payload }) {
12201251
return;
12211252
}
12221253

1223-
resetAutoLock();
12241254
await forceRelease(); // Clean up previous tab if it closed without cleaning itself up
12251255
prompt.release = await prompt.mutex.acquire();
12261256

0 commit comments

Comments
 (0)