{"record":{"id":"50758e33c9a5b259","repo":"gildas-lormeau/SingleFile","slug":"unauthorized-sender","errorCode":null,"errorMessage":"Unauthorized sender","messagePattern":"Unauthorized sender","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/core/bg/external-capture-permissions.js","lineNumber":68,"sourceCode":"\t\tthrow new Error(\"Cannot identify the extension requesting SingleFile capture\");\n\t}\n\tconst permissions = await config.getExternalCapturePermissions();\n\tif (permissions.allowedExtensionIds.includes(extensionId)) {\n\t\treturn true;\n\t}\n\tif (permissions.deniedExtensionIds.includes(extensionId)) {\n\t\treturn false;\n\t}\n\tconst { request, created } = getOrCreatePendingRequest(extensionId, sender, message);\n\tif (created) {\n\t\tawait openOptionsPage(request);\n\t}\n\treturn request.promise;\n}\n\nasync function onMessage(message, sender) {\n\tif (!isOptionsPageSender(sender)) {\n\t\tthrow new Error(\"Unauthorized sender\");\n\t}\n\tif (message.method.endsWith(\".getPermissions\")) {\n\t\treturn config.getExternalCapturePermissions();\n\t}\n\tif (message.method.endsWith(\".setPermissions\")) {\n\t\tawait config.setExternalCapturePermissions(message.permissions);\n\t\treturn {};\n\t}\n\tif (message.method.endsWith(\".getPendingRequest\")) {\n\t\treturn getPendingRequest(message.requestId);\n\t}\n\tif (message.method.endsWith(\".respondPendingRequest\")) {\n\t\treturn respondPendingRequest(message.requestId, message.approved);\n\t}\n}\n\nfunction isOptionsPageSender(sender) {\n\treturn Boolean(sender) && sender.id == browser.runtime.id &&","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/gildas-lormeau/SingleFile/blob/517fb7c5cf2096d89933b747e862d8ecf616a9f9/src/core/bg/external-capture-permissions.js#L50-L86","documentation":"SingleFile's background message handler for external capture (onMessage in external-capture-permissions.js) only accepts messages originating from the extension's own options page. If the sender's tab/url does not match the options page, it throws 'Unauthorized sender'. This guards the permission-setting API from being invoked by arbitrary web pages or other extension contexts.","triggerScenarios":"Calling browser.runtime.sendMessage from a context whose sender is not the options page (e.g. a content script, popup, or another extension) with a method ending in .getPermissions or .setPermissions.","commonSituations":"Developers wiring up external capture automation call setPermissions from a script or console in the wrong context; tests invoke the handler directly with a fabricated sender object lacking options-page URL/id.","solutions":["Send the message from the extension's options page context (or open the options page and run the call there).","If calling programmatically, obtain the options page's tab and use browser.tabs.sendMessage to it so isOptionsPageSender passes.","In tests, construct a sender object matching what isOptionsPageSender expects (correct url/id fields)."],"exampleFix":"// before (from a content script)\nbrowser.runtime.sendMessage({ method: 'externalCapture.setPermissions', permissions: [...] });\n// after (from the options page itself)\nconst tabs = await browser.tabs.query({ url: browser.runtime.getURL('options/index.html') });\nawait browser.tabs.sendMessage(tabs[0].id, { method: 'externalCapture.setPermissions', permissions: [...] });","handlingStrategy":"validation","validationCode":"function isOptionsPageSender(sender) {\n  const optionsUrl = browser.runtime.getURL('options/index.html');\n  return sender && sender.tab && sender.url && sender.url.startsWith(optionsUrl);\n}\n// only sendMessage when this returns true","typeGuard":"function isFromOptionsPage(sender) {\n  return typeof sender?.url === 'string' &&\n    sender.url.startsWith(browser.runtime.getURL('options/'));\n}","tryCatchPattern":"try {\n  await browser.runtime.sendMessage({ method: 'externalCapture.setPermissions', permissions });\n} catch (e) {\n  if (e.message === 'Unauthorized sender') {\n    // move the call into the options page context\n  }\n}","preventionTips":["Only call permission APIs from the options page context","Verify sender.url against the options page URL before sending","In tests, build sender objects that match isOptionsPageSender expectations"],"tags":["permissions","extension","sender-validation"],"backgroundTag":"unauthorized-sender","analyzedSha":"517fb7c5cf2096d89933b747e862d8ecf616a9f9","analyzedAt":"2026-09-01T10:05:25.770Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}