FriendlyCaptcha / friendly-challenge

The widget and docs for the proof of work challenge used in Friendly Captcha. Protect your websites and online services from spam and abuse with Friendly Captcha, a privacy-first anti-bot solution.
https://friendlycaptcha.com
MIT License
414 stars 61 forks source link

Content Security Policy and 'unsafe-eval' #46

Closed cicdht closed 3 years ago

cicdht commented 3 years ago

We are trying to add FriendlyCaptcha to our website, but our CSP blocks the execution of the script with the following error message: "Refused to compile or instantiate WebAssembly module because 'unsafe-eval' is not an allowed source". Is there a precompiled version of the script available that doesn't need the 'unsafe-eval' option? Add this option to the CSP is not an option. Thank you for your help.

gzuidhof commented 3 years ago

Hi @cicdht,

Just to let you know I'm looking into this now, apologies for the 5 day delay (I was on a short holiday).

Probably the easiest solution is for us to calculate the hash of both the worker script (for CSP rules around worker-src) and the WASM module and publish these in our documentation for different versions. Then you could add script-src sha256-somehash and be up and running :).

The alternative option would be to provide a separate .wasm file, as well as a separate worker.js file (currently these are encoded as strings), but I hope that's not necessary as it would complicate setup (as they need to point at eachother).

I expect I'll get something up and running today or tomorrow.

gzuidhof commented 3 years ago

An update:

I'm stumped on the implementation in Chrome for allowing WebAssembly to be compiled without unsafe-eval. I imagined that adding a SRI hash would do the trick, which can be calculated as follows:

// Replacing the lines in widget.module.js around line 459
const b = decode(base64);
crypto.subtle.digest("SHA-256", b).then(x => console.log("Hash:", encode(new Uint8Array(x))));
const module = WebAssembly.compile(b);

which prints

Hash: +ECqXea/yNwIUs7thEFTUWhwor8TToKOrr9J1xrj9zw=

To verify this hacky inline code, we can also do it on the command-line

>> openssl sha256 -binary node_modules/friendly-pow/wasm/optimized.wasm | openssl base64
+ECqXea/yNwIUs7thEFTUWhwor8TToKOrr9J1xrj9zw=

Then setting

<meta http-equiv="Content-Security-Policy" content="script-src 'sha256-+ECqXea/yNwIUs7thEFTUWhwor8TToKOrr9J1xrj9zw='; worker-src blob:">

Chrome still tells me: CompileError: WebAssembly.compile(): Wasm code generation disallowed by embedder.


Resources:

This was an unexpected rabbit hole to go down, and I'm afraid for now there is no solution that doesn't involve adding unsafe-eval for Chrome users on pages where you are embedding a captcha widget for the best user experience.

Note that the widget will still work on Chrome even without it - it will use the Javascript fallback instead of the WebAssembly version, which is ~10 times slower, so user experience will be (unnecessarily) poor until Chrome gets their act together :(.

gzuidhof commented 3 years ago

Thinking about this a bit more, there is a possible workaround without putting 'unsafe-eval' on your webpage. You can serve the widget in an iframe (which has unsafe-eval or no CSP at all) and communicate with the parent frame over postMessage.

cicdht commented 3 years ago

Hi @gzuidhof Thank you for your answers, I really appreciate your digging into this issue. As far as I understood your comments it looks as if this is an issue with the implementation of WebAssembly and cannot be bypassed? Your solution would try to iframe the FriendlyCaptcha div. Any idea why the captcha script is blocked at the same line not only in Chrome (if it is a Chrome bug) but also in FF and Safari?

gzuidhof commented 3 years ago

You could say it's more a gap in the standard (that fortunately the browser vendors are closing, but it could take a bit of time).

It looks like there is no way to have some WASM code executed in a page which has a CSP that is strict around script-src without adding unsafe-eval. For (inline) JS code you can add a hash of the code, or a nonce, but that is not supported for WASM (yet).

It may also be blocked in FF and Safari - when trying to find a workaround it seemed to work in FF for me (I may be wrong!), I didn't try it in Safari. So I assumed that FF wasn't as strict as Chrome.


Maybe it makes sense if we provide an iframe HTML file ready for use with this library (or at least provide an example), perhaps we can even encode it in the src or srcdoc as a string so there won't actually be a need to load another page - but that is to be seen (if you do so, the origin of the page is null, which tends to cause problems when creating webworkers).

cicdht commented 3 years ago

Any chance you will be providing such an option in the near future? This would facilitate implementation and would help future customers who might be running into a similar issue.

gzuidhof commented 3 years ago

We would love to provide such an option in the future, but I can't make promises in terms of timeline (it could be weeks or a few months..). In the meantime I am happy to receive it as a contribution (of course I can provide pointers and help here and there).

cicdht commented 3 years ago

I checked the principle with an iframe of a page on the same host and - even though promising - it doesn't solve the CSP problem. The error still appears. Maybe this changes if the script source is external? So I will have to find another solution. Sorry, but thanks for the helpful suggestions.

papegaaij commented 3 years ago

Hitting this very same issue, I may have found a route that bypasses this restriction: WebAssembly.compile does seem to work inside a web worker. I looked at https://github.com/hackcasual/ChromeWASMCSP and didn't understand why it actually worked, so I copied it inside our application and it still worked. Next, I copied the WebAssembly.compile in the head, and sure enough it breaks with CompileError: WebAssembly.compile(): Wasm code generation disallowed by embedder. So I think the solution to this problem for friendly-challenge is to move the invocation of WebAssembly.compile() into the worker it already has.

papegaaij commented 3 years ago

@gzuidhof I've just checked version 0.9.0, and the call to WebAssembly.compile() still happens from the context of the webpage (directly from the invocation of new friendlyChallenge.WidgetInstance). Did I miss something? Your comment in the PR seems to suggest this call was moved to a webworker.

papegaaij commented 3 years ago

Ok, nevermind, it was my development setup keeping deploying a stale version of the file. The setup works perfectly with chrome and firefox. In fact, the captcha is now so fast you can hardly see it working. For Safari, I need some more time to test it, because I don't have access to a Mac.

papegaaij commented 3 years ago

We can confirm that this solution also works in Safari with a CSP that doesn't contain unsafe-eval. Keep in mind that Safari doesn't support the worker-src directive, so for Safari, you'll need to add blob: to child-src, otherwise Safari will block the worker.

dev-love commented 3 years ago

@gzuidhof Could you have a look at the Safari worker support? Seems to be an important thing to me 👍🏼

papegaaij commented 3 years ago

@dev-love version 0.90.0 works perfectly fine with Safari. You'll need to setup your CSP to include blob: in child-src, but that's not a big problem and not something that can be changed in friendly-challenge. Safari needs to add support for the worker-src CSP directive.

dev-love commented 3 years ago

@papegaaij Thanks for the clarification! :)

gzuidhof commented 3 years ago

Happy it works now! Thank you for the detailed investigation and making sure it works now, and of course for proposing the solution!

I'll be closing this issue now, but feel free to re-open (or comment) if you run into any other CSP-related problems.

papegaaij commented 3 years ago

It seems the Chromium developers already caught up with this fix. In Chrome 96 (currently beta), this no longer works. I suspect they now correctly implement the rule mentioned here: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#content_security_policy The exception to this is if the worker script's origin is a globally unique identifier (for example, if its URL has a scheme of data or blob). In this case, the worker does inherit the CSP of the document or worker that created it.

I fear, the only way to get this working again is to move the webworker source to a separate file. That would allow setting a different (less restrictive) CSP on the webworker source. I'll see if I can get that to work locally and report back with the results.

papegaaij commented 3 years ago

According to this bug https://bugs.chromium.org/p/chromium/issues/detail?id=1259726 the change was intentional. Unfortunately, placing the worker in its own file does not help, because of this issue: https://bugs.chromium.org/p/chromium/issues/detail?id=567999&q=worker%20csp&can=2#c21

It seems the chrome developers made it impossible to use wasm without adding unsafe-eval again. It does however seem they are working on proper wasm support in the CSP, which they hope to deliver in 97 (but probably still behind a flag, so still useless).

gzuidhof commented 3 years ago

Hi again Emond,

Sorry to hear that, that's really annoying :(. Of course we can provide the worker as a standalone file so you can serve it from your own origin - but from what I understand even that won't cut it.

Also with all these changes: we hope to support pretty much every browser in the last 10 years or so - even if they do change the CSP strictness now, there will be people on the older versions for a long time :/

Thank you for investigating! I'm happy to open a new issue if you think that makes sense.

papegaaij commented 3 years ago

Yes, this is getting really frustrating. I think the only thing to do here is wait for the Chrome developers to make up their minds and come with a real solution. From what I've read, a first solution may be available in 97, but probably still behind a flag. It might be needed to put the worker in a different file, but at the moment that doesn't help, because the worker always (incorrectly) shares the policy with its parent document. I'll monitor the progress in Chrome and report back when a solution is available.

ellotheth commented 2 years ago

For what it's worth, child-src blob: and script-src 'wasm-unsafe-eval' get this working properly in Chrome 97. Firefox (97) is unhappy with 'wasm-unsafe-eval' but it doesn't seem to break anything. Safari 15 still doesn't recognize wasm-unsafe-eval and falls back to the JS solver.

chess-levin commented 2 years ago

Hi, I' d like to recommend FC to a customer. The customers website uses a strict content security policy (meaning no inline script allowed). While googling this topic I found this issues and like to ask what is the current state of this topic?

It would be really helpful to explain how to use/install FC when the site uses a (strict) content security policy on your installation page . There should be a topic requirements for integration.

Thanks for your support.

gzuidhof commented 2 years ago

I agree that we should have a docs page explaining how to use Friendly Captcha with various CSP settings. Gradually we have introduced changes that made it more strict CSP friendly - but I understand that going through those Github issues is hardly a nice way to figure out the details. It's been on my to do list for way too long (the effort is mostly in verifying in different browsers, I don't want to put an incomplete set of instructions that doesn't work in some browsers).

I'm more than happy to accept any contributions of setups that have worked for others and compile them onto a dedicated docs page.

We've been hard at work the past months in building out a second version of our offering, which will be more easily CSP compatible, by sandboxing most of our logic into an iframe hosted on our own origin - that shouldn't have to deal with any of the wasm-unsafe-eval browser differences.

snabela commented 1 year ago

@gzuidhof are there any news on the solution which offers more simple setup for CSP compliance? We are replacing Google ReCaptcha for friendly captcha, but have a strict CSP which I do not want to loosen unnecessarily.

gzuidhof commented 1 year ago

Hi Alexander,

We have since written a page on CSP: https://docs.friendlycaptcha.com/#/csp, hopefully one of the options provided fits your existing CSP without relaxing it unnecessarily.

If one has strict security requirements, I would suggest to serve the JS files from your own server (or bundle it into your existing JS bundle).

With that setup, the code is immutable, it can not possibly change under your feet. As all the Friendly Captcha code running on your website is open source, it can also be vetted. With such a setup you won't have to add a rule for a CDN.

This "fixed script" is something that other captcha systems can not offer (reCAPTCHA or otherwise). Most other captcha systems load scripts dynamically (e.g. it loads https://www.google.com/recaptcha/api.js), and if that api.js ever gets replaced by the contents of evil.js your website is compromised regardless of CSP settings..


A sneak peek: in our upcoming v2 we further simplified the CSP, only requiring a rule for allowing sandboxed iframes: https://friendlycaptcha.github.io/friendly-docs/docs/sdk/advanced/csp (link will likely be broken later).

V2 isn't publicly available yet, but at least it's something to look forward to (also for ourselves :)).

Let me know if something is unclear or if I missed anything.

snabela commented 1 year ago

Hi Guido, many thanks for your detailed feedback!

We are using the friendly challenge npm package, i.e. we have no need for the CSP rule to download the script from your CDN.

I am looking forward to V2, which should allow us to again remove the following from our CSP (if I understood correctly): script-src 'wasm-unsafe-eval' 'self'; worker-src blob:; child-src blob:

Please note that your CSP documentation seems to miss a rule for the puzzle call. We had to add https://api.friendlycaptcha.com/ to connect-src. We worked around this requirement by adding a forwarding rule for the puzzle endpoint to our CDN.

When do you expect V2 to be released?

snabela commented 1 year ago

@gzuidhof would have some hint about a potential release date for V2?

gzuidhof commented 1 year ago

Hi Alexander,

I'm sorry about the delay in response, I'm hesitant to put a date on it. We need to make sure that it is reliable, robust, and compatible with really old browsers as well. Aside from that, we also know that making breaking changes is easy now, but is something to be really avoided once it is publicly available.

For those reasons I would rather I don't promise a date.. I would say something vague like "this year", but I'm not sure how useful that is. I hope you can understand.