PaddleHQ / paddle-js-wrapper

Wrapper to load Paddle.js as a module and use TypeScript definitions when working with methods.
Apache License 2.0
41 stars 6 forks source link

[Bug]: Content-Security-Policy blocks access. #22

Open zachwhelchel opened 8 months ago

zachwhelchel commented 8 months ago

What happened?

I guess this is a CORS issue with my React app? But some guidance on this would be helpful.

GET https://cdn.paddle.com/paddle/v2/paddle.js net::ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep 200 (OK)

I've tried modifying my Content-Security-Policy to this: <meta http-equiv="Content-Security-Policy" content="script-src 'self' https://cdn.paddle.com/; worker-src 'self' blob:;"> But it doesn't seem to help. Any thoughts? Could the code that downloads the script in the package be marked specifically with some modifier that CORS allows? I've seen that done elsewhere.

Steps to reproduce

Use the package via yarn install in a React app that has a strict Content-Security-Policy.

What did you expect to happen?

No response

How are you integrating?

No response

Logs

No response

vijayasingam-paddle commented 8 months ago

Hi @zachwhelchel, We are more than happy to help.

Looking at the error code NotSameOriginAfterDefaultedToSameOriginByCoep, I am guessing you have a Cross-Origin-Embedder-Policy header set as require-corp.

If yes, can you please change it to credentialless along with the Content-Security-Policy change you already had in your question to allow scripts from https://cdn.paddle.com/

if not, please share all the security headers you have in your application so that we can check further.

Thank you.

zachwhelchel commented 7 months ago

@vijayasingam-paddle thanks for the response. I set the Cross-Origin-Embedder-Policy as you suggested and it gets further now... but when I try to initialize a checkout the window shows this:

Screenshot 2024-02-09 at 8 44 49 AM

There are no errors given in the console so I'm not sure what to do next.

zachwhelchel commented 7 months ago

@vijayasingam-paddle I just checked the documentation for my code base and apparently there are restrictions on the CORS changes I can make:

In addition to the HTTPS requirement, the Cross-Origin-Embedder-Policy and Cross-Origin-Opener-Policy headers must be set to require-corp and same-origin respectively.

Is there any way around this from your all's end? Does Paddle provide a hosted checkout page? Where I could direct users to with some prefilled options? Any way I try this I can't seem to get paddle to load on my app.

vijayasingam-paddle commented 7 months ago

Hi @zachwhelchel, Sorry, we do not provide hosted checkout pages in Paddle.

Changing CORS headers is a very common approach to include third party scripts (Including Google Analytics or Cookie consent popup providers) to our applications. So it should be alright to change the CORS headers to allow downloading scripts from our domain.

Unfortunately, I don't see any other option to make it work.

but when I try to initialize a checkout the window shows this:

This is strange, we suspect a firewall or some other issue in your end.

What happens when you open https://buy.paddle.com/ in your browser? it should throw a Something went wrong as it is missing some input not relevant to our test to see if you have a problem on your end.

zachwhelchel commented 7 months ago

@vijayasingam-paddle this is what I see when I go to that url. So it seems to load in another tab but in my app it's still giving the "refused to connect" error I posted above. Is there some other security setting that needs to be set that we haven't covered?

Screenshot 2024-02-27 at 2 57 39 PM
zachwhelchel commented 7 months ago

@vijayasingam-paddle I thought it might be due to a "frame-src" in the Content-Security-Policy but that doesn't seem to be the case.

zachwhelchel commented 7 months ago

I also tried adding this and it didn't change anything:

frame-ancestors 'self' https://sandbox-buy.paddle.com/;

zachwhelchel commented 7 months ago

@vijayasingam-paddle welp... it turns out that Safari is still not pleased with my changes to "credentialless". We use the Shared Array Buffer and "credentialless" removes access to it for security purposes.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer

Is there any chance I can set up a developer support call or something? I really want to implement Paddle but I just can't seem to get it working!

vijayasingam-paddle commented 7 months ago

Hi @zachwhelchel, I am sorry to hear that you are getting stuck while integrating with Paddle.

Can you please reach out to us at sellers@paddle.com and one of our support specialist will be able to help you with the integration.

Thank you.

zachwhelchel commented 7 months ago

@vijayasingam-paddle I reached out to that email address asking for help. Thanks! Maybe it's just not possible to both support SharedArrayBuffer and Paddle inside the same app? Becuase the second I change to "credentialless" Safari refuses to use the SharedArrayBuffer. But "credentialless" is nessecary for Paddle? Am I missing something here or does Paddle just not work in any app that needs to support SharedArrayBuffer?

thomasdondorf commented 6 months ago

Hi @vijayasingam-paddle

I'm trying to integrate Paddle and have the same problem as @zachwhelchel.

The problem is that Paddle is currently not following security best-practices by not providing the Cross-Origin-Resource-Policy header.

A more in-depth explanation as I would be interested in this getting solved by Paddle:

TLDR: The problem is on paddle's side.

If this is not clear enough, check out this website that explains it: https://resourcepolicy.fyi/

And just to understand that this is best practice to deliver the Cross-Origin-Resource-Policy: cross-origin to embed resources, you can check any package on any CDN like cdnjs or jsdelivr. These will all provide that header as otherwise some websites will not be able to embed them.

How to fix this: Paddle needs to add the header Cross-Origin-Resource-Policy: cross-origin to all resources on cdn.paddle.com.

It would be great if Padde could fix this.

zachwhelchel commented 6 months ago

@thomasdondorf thanks for jumping in here. Just an update from my end. I was never able to get this working... I had to opt to sending users to a page on my marketing site to process the payment. Not ideal. Would love to see this fixed.

thomasdondorf commented 6 months ago

@zachwhelchel Sure, I was happy someone had already reported the problem :)

The only way to solve this currently is to redirect from the "main page" to some special page with the policy disabled...

vijayasingam-paddle commented 6 months ago

Hi @zachwhelchel, I am so sorry, i was with the impression that our support team was working with you to solve the problem. I didn't know that it was still open.

Hi @thomasdondorf, Thank you for sharing a very detailed report.

I will check with our support team to find why it was not addressed and move the ticket back to engineering to fix this problem.

Thank you for your patience, I am really sorry for the inconvenience.

pedrovgs commented 4 months ago

This one is super interesting for me 😃 I hope we can get it working soon

vijayasingam-paddle commented 4 months ago

Hi everyone, Sorry for the long silence on this issue. I am working with our security team internally to come with the best solution that works for everyone and will update you once i have something concrete.

Thank you for waiting patiently.

marcolarosa commented 4 months ago

I'm seeing a CSP issue too. Not sure if it's the same problem as that discussed here but I'll report just in case it's related.

I have paddle integrated into an electron app that has the following CSP:

 <meta
            http-equiv="Content-Security-Policy"
            content="default-src 'self' https://describo.github.io https://stamen-tiles-a.a.ssl.fastly.net/ https://api.github.com https://raw.githubusercontent.com https://api.ror.org;
            script-src 'self' https://cdn.paddle.com/paddle/v2/paddle.js https://public.profitwell.com/js/profitwell.js;
            style-src 'self' 'unsafe-inline' https://sandbox-cdn.paddle.com/paddle/v2/assets/css/paddle.css https://cdn.paddle.com/paddle/v2/assets/css/paddle.css;
            connect-src https://www2.profitwell.com;
            img-src * crate-file: data:;
            media-src crate-file:;
            frame-src https://buy.paddle.com https://sandbox-buy.paddle.com;"
        />

In dev, connecting to the paddle sandbox works fine but when the app is built, it refuses to connect to paddle prod.

In the console I see: [Report Only] Refused to frame 'https://buy.paddle.com/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors ". Note that '' matches only URLs with network schemes ('http', 'https', 'ws', 'wss'), or URLs whose scheme matches self's scheme. The scheme 'https:' must be added explicitly.

[Report Only] Refused to frame 'https://buy.paddle.com/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors ". Note that '' matches only URLs with network schemes ('http', 'https', 'ws', 'wss'), or URLs whose scheme matches self's scheme. The scheme 'https:' must be added explicitly.

vijayasingam-paddle commented 4 months ago

Hi @marcolarosa, You are seeing a different error and not related to this GH issue. As the log says, the message you see is [Report Only] and should not be blocking your request. With that said, we found that our checkout doesn't open within an electron app because of the domain checks.

Our Checkouts can be launched only from approved domains in Production. However, as electron apps do not have a domain, our validation is failing and throwing an error.

I am sorry that you are running into this issue. I am checking with the team about fixing this issue. Meanwhile, please open a different issue in GH to track this independently.

Thank you.

marcolarosa commented 4 months ago

@vijayasingam-paddle Ahh sorry! Thanks for the advice.

vijayasingam-paddle commented 4 months ago

Hi @zachwhelchel / @thomasdondorf / @pedrovgs,

Thank you for being patient while we were looking into this issue. Unfortunately, I have some bad news. I am afraid we won't be able to make our checkouts work with cross-origin isolation.

Changing the CORS headers in the Paddle side of things was the easier change. However, we quickly found that the problems don't stop there.

In a cross-origin isolated environment, all the iframes loaded into the browser will inherit the same COEP and should either be from the same domain or include appropriate CORS headers. Even though we could fix the headers on our end, we rely on other payment partners and their servers do not send any CORS headers.

Due to this, with the correct CORP and COEP headers we will be able to run Paddle.js and launch checkout. However, it will fail when it reaches the 3DS payments screen with the same error.

There is a proposal to allow iframes to act independently, though browser support is minimal as it works only with Chrome at the moment. We cannot choose this solution as we will have to support all the browsers.

Sadly, there is very little we can do about this issue. We will keep tabs on the proposal and we could get it working once every major browsers start supporting it.

Please let us know if we can help you with anything else. Thank you

lowkahonn commented 4 months ago

@vijayasingam-paddle would it be possible to allow opt-in for using the credentialless iframes?

vijayasingam-paddle commented 4 months ago

Hi @lowkahonn, That is something we can do. However, we would probably not recommend this for most users because this will hit the conversion rate drastically.

I will get this working in a couple of weeks and maybe we could run some beta tests to see real-world impact.

Thank you.

reknih commented 4 months ago

Would there be an option to perform feature detection for credentialless iFrames and use them if present?

Currently, our web app (SPA) is Cross-Origin isolated and when a user opens the checkout page, we need to refresh the page to drop the COEP and COOP headers. When the user navigates away, we reload again. As this differs from SPA navigation, it can be jarring to our users. We would like to offer credentialless checkout frames to those users whose browsers support them and use our hard reload technique for all other users.

vijayasingam-paddle commented 4 months ago

Hi @reknih, We would like to keep the library pure and move away from any side effects based on the browser. We won't be auto-detecting this property based on the browser and would like our integrators to pass the value when they need it.

I found this snippet to detect if an element supports a property. You can use this to detect if iframe supports credentialless and pass it to the new attribute we will add to PaddleJS.

Thank you.

thomasdondorf commented 4 months ago

I was able to integrate the payment process relatively smoothly in the following way. I added an exception in my server configuration to not serve the "Cross-Origin" headers for the one page that integrates Paddle.js (/checkout in my example).

Here is my (simplified) nginx config.

location = /checkout {
    try_files /checkout.html =404;
}

location / {
    add_header Cross-Origin-Opener-Policy "same-origin";
    add_header Cross-Origin-Embedder-Policy "require-corp";

    try_files $uri.html =404;
}

Note for Next.js users (like myself): Make sure to use actual links (a instead of Link) and "real" redirects (location.assign/location.replace instead of router.push/router.replace) when linking to the Paddle page as otherwise the headers will not be loaded.

Maybe this helps someone else.

thomasdondorf commented 4 months ago

As someone reached out to me for help, I'll give more details below:

You can see it in action here: https://www.chessmonitor.com/pricing, but you have to create a (free) account. I hope it's okay that I link it here (if you are a chess player, check it out 😉).

Basically there are two pages:

And of course, you should setup an event listener to listen to events like checkout.completed/checkout.closed to forward the user back.

Hope that makes it more clear!