Blazor.BrowserExtension icon indicating copy to clipboard operation
Blazor.BrowserExtension copied to clipboard

Sidebar-ContentScript (manifest V3) not working

Open radiolondra opened this issue 3 years ago • 14 comments

Just to confirm this post on Samples. The error is always the same (see picture below) This happens while opening the majority of websites (e.g. github.com and tons of others).

mingError

radiolondra avatar Nov 17 '22 09:11 radiolondra

Installing my test extension on Edge I can see at least the meaning of the exception:

mingErrorEdge

It seems that somewhere in code ther's still a usage of eval. With Manifest V3 unsafe-eval in content-security-police is not allowed (and doesn't exist) anymore. And the error is thrown. Using Manifest V2 (now dead) everything works.

Any idea about how to solve this?

radiolondra avatar Nov 17 '22 12:11 radiolondra

I have investigated with a simple wasm extension and it produces the same behaviour. I have filed a bug report in Chromium here.

mingyaulee avatar Nov 17 '22 15:11 mingyaulee

@mingyaulee or @radiolondra with this Chromium bug in place, does it mean that no Blazor based browser extension will work with Manifest V3?

scottkuhl avatar Aug 22 '23 18:08 scottkuhl

All web assembly extensions, not just Blazor, are affected by this bug. The extensions that loads web assembly in content scripts are subjected to the CSP of the page the user is browsing, instead of the CSP declared in the extension manifest. Therefore, the content script works in some pages and not in those with strict CSP.

mingyaulee avatar Aug 22 '23 21:08 mingyaulee

Thanks @mingyaulee . That's quite the show stopper for web assembly based extensions.

scottkuhl avatar Aug 22 '23 22:08 scottkuhl

"The extensions that loads web assembly in content scripts are subjected to the CSP of the page the user is browsing"

@mingyaulee so could you create a browser extension with Blazor, as long as the content scripts were written in just JavaScript?

"content_scripts": [
  {
    "matches": [ "*://*/*" ],
    "js": [ "ContentScript.js" ]
  }
],

scottkuhl avatar Oct 10 '23 18:10 scottkuhl

"The extensions that loads web assembly in content scripts are subjected to the CSP of the page the user is browsing"

@mingyaulee so could you create a browser extension with Blazor, as long as the content scripts were written in just JavaScript?

"content_scripts": [
  {
    "matches": [ "*://*/*" ],
    "js": [ "ContentScript.js" ]
  }
],

Yes that is correct.

mingyaulee avatar Oct 10 '23 19:10 mingyaulee

I have my Blazor WASM V3 browser extension working on every page using the below declarativeNetRequest rule to remove CSP rules in page response headers. Not a perfect solution, but it works now.

{
   id: 1,
   action: {
       type: 'modifyHeaders',
       responseHeaders: [
           {
               header: 'content-security-policy',
               operation: 'remove'
           }
       ]

   },
   condition: {
       urlFilter: "|https*",
       resourceTypes: ["main_frame", "sub_frame"]
   }
}

I am not using this repo but it is helpful. @mingyaulee Thank you.

LostBeard avatar Feb 07 '24 20:02 LostBeard

I have my Blazor WASM V3 browser extension working on every page using the below declarativeNetRequest rule to remove CSP rules in page response headers. Not a perfect solution, but it works now.

{
   id: 1,
   action: {
       type: 'modifyHeaders',
       responseHeaders: [
           {
               header: 'content-security-policy',
               operation: 'remove'
           }
       ]

   },
   condition: {
       urlFilter: "|https*",
       resourceTypes: ["main_frame", "sub_frame"]
   }
}

I am not using this repo but it is helpful. @mingyaulee Thank you.

I'd imagine this would be rejected when publishing to the Chrome extension store because installing the extension which removes the content security policy header will make the user's browser vulnerable to XSS attacks.

mingyaulee avatar Feb 13 '24 09:02 mingyaulee

I'm sure it would be.

Another method is to have the content page message the background script (extension service worker) when loading Blazor WASM fails due to CSP by listening for the onsecuritypolicyviolation event. Then the background script can add 'wasm-unsafe-eval' to the 'originalPolicy' (supplied by the onsecuritypolicyviolation event) 'script-src' section and create a dynamic rule with a urlFilter for that page that will 'set' the 'content-security-policy' header to your updated security policy next page load.

Still not ideal, but much better than removing CSP completely or not working at all.

LostBeard avatar Feb 14 '24 04:02 LostBeard

I am using github.com to test fixes / workarounds for the WebAssembly.instantiateStreaming issue due to CSP violations in extension content mode. My Blazor extension is working on github.com with the method above.

Hopefully they get this fixed soon.

@mingyaulee Thank you for filing the bug report and getting that ball rolling.

LostBeard avatar Feb 14 '24 16:02 LostBeard

The below is working in Chrome. I haven't tested Firefox yet. This patches CSP rules to include 'wasm-unsafe-eval' and reloads the tab to allow Blazor to load.

in content script before trying to load Blazor WASM

function onSecurityPolicyViolation(e) {
    // chrome - e.blockedURI === "wasm-eval"
    if ((e.blockedURI === "wasm-eval" || e.blockedURI === "wasm-unsafe-eval")
        && e.violatedDirective === "script-src"
        && e.originalPolicy.indexOf('wasm-unsafe-eval') === -1) {
        document.removeEventListener('securitypolicyviolation', onSecurityPolicyViolation);
        var cspViolation = {
            documentURI: e.documentURI,                 // document.location.href
            originalPolicy: e.originalPolicy,           // csp header value
            // atm only the above to vars are used in the background script. below are informative
            blockedURI: e.blockedURI,                   // "wasm-eval"
            disposition: e.disposition,                 // "enforce"
            effectiveDirective: e.effectiveDirective,   // "script-src"
            sourceFile: e.sourceFile,                   // "chrome-extension" (not sure on firefox, others)
            violatedDirective: e.violatedDirective,     // "script-src"
        };
        browser.runtime.sendMessage({ cspViolation });
    }
}
document.addEventListener('securitypolicyviolation', onSecurityPolicyViolation);

in background (service worker) script

browser.runtime.onMessage.addListener(function (request, sender) {
    if (request.cspViolation) {
        patchCSP(request, sender);
    }
});
async function patchCSP(request, sender) {
    if (request.cspViolation) {
        var originalPolicy = request.cspViolation.originalPolicy;
        var updatedPolicy = originalPolicy;
        if (originalPolicy.indexOf('wasm-unsafe-eval') === -1) {
            updatedPolicy = originalPolicy.replace('script-src ', "script-src 'wasm-unsafe-eval' ");
        } else {
            // rule already has 'wasm-unsafe-eval'
            // if this happens, there is another problem
            return;
        }
        var url = new URL(request.cspViolation.documentURI);
        // separate paths on the same domain may have different csp rules
        // the query string and hash shouldn't have any effect on csp rules 
        var pageUrl = url.origin + url.pathname;
        var pageUrlEscaped = escapeRegExp(pageUrl);
        var cspRule = {
            id: await getFreeSessionRuleId(),
            action: {
                type: 'modifyHeaders',
                responseHeaders: [
                    {
                        header: 'content-security-policy',
                        operation: 'set',
                        value: updatedPolicy,
                    }
                ]

            },
            condition: {
                regexFilter: `^${pageUrlEscaped}(\\?.*)?(#.*)?$`,
                resourceTypes: ["main_frame", "sub_frame", "xmlhttprequest"]
            }
        };
        console.log('Adding rule', cspRule);
        // save rule
        await browser.declarativeNetRequest.updateSessionRules({
            addRules: [cspRule]
        });
        // reload tab so the new rule can take effect
        browser.tabs.reload(sender.tab.id);
    }
}
async function getFreeSessionRuleId() {
    var previousRules = await browser.declarativeNetRequest.getDynamicRules();
    var previousRuleIds = previousRules.map(rule => rule.id);
    if (previousRuleIds.length === browser.declarativeNetRequest.MAX_NUMBER_OF_SESSION_RULES) {
        await browser.declarativeNetRequest.updateSessionRules({ removeRules: previousRuleIds });
        previousRuleIds = [];
    }
    var availId = Math.floor(Math.random() * 1000000) + 1;
    while (previousRuleIds.indexOf(availId) !== -1) availId = Math.floor(Math.random() * 1000000) + 1;
    return availId;
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
function escapeRegExp(string) {
    return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}

LostBeard avatar Feb 15 '24 00:02 LostBeard

From my understanding of the API, the rules are isolated from other extensions, so you can just keep track of the latest running ID in the local storage.

mingyaulee avatar Feb 15 '24 08:02 mingyaulee

After finally getting my Blazor extension working in Firefox I am surprised to see that Firefox does not have the 'wasm-unsafe-eval' issue that Chrome has. Tested GitHub and it is working. Yay!

What was surprising is that, in extension content scripts/pages, Firefox wraps the return value from Resposne.json, Resposne.arrayBuffer, and Resposne.blob in an XrayWrapper. The cryptic error Error: Not allowed to define cross-origin object as property on [Object] or [Array] XrayWrapper was hard to diagnose. All because Blazor was fetching some json and then trying to modify it (blazor.boot.json.)

LostBeard avatar Feb 17 '24 21:02 LostBeard