feat(paywalls): isolate web_view content with a fixed CSP (PWENG-98)#3652
feat(paywalls): isolate web_view content with a fixed CSP (PWENG-98)#3652alexrepty wants to merge 1 commit into
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
66c0ccb to
a3e9e1a
Compare
61039df to
38f5302
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## alexrepty/paywalls-web-view-rendering #3652 +/- ##
======================================================================
Coverage 80.32% 80.32%
======================================================================
Files 385 385
Lines 15845 15845
Branches 2225 2225
======================================================================
Hits 12727 12727
Misses 2229 2229
Partials 889 889 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
38f5302 to
6c73e7c
Compare
a3e9e1a to
0dffacc
Compare
6c73e7c to
71e76e1
Compare
0dffacc to
353f584
Compare
353f584 to
df0ae01
Compare
88cb259 to
fd0371b
Compare
df0ae01 to
22be911
Compare
22be911 to
9cff885
Compare
fd0371b to
bc0f4e0
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9cff885. Configure here.
| meta.setAttribute('http-equiv', 'Content-Security-Policy'); | ||
| meta.setAttribute('content', "${policy.escapeForMetaContent()}"); | ||
| var head = document.head || document.getElementsByTagName('head')[0] || document.documentElement; | ||
| if (head.firstChild) { head.insertBefore(meta, head.firstChild); } else { head.appendChild(meta); } |
There was a problem hiding this comment.
CSP meta placed outside head
High Severity
When document.head is not yet available, the injection script falls back to document.documentElement and inserts the CSP meta as a direct child of html. CSP meta tags are only applied when they live inside head, so this placement is ignored and isolation does not take effect. Setting window.__revenueCatCspInstalled on that failed attempt can also block later injections on the same navigation.
Reviewed by Cursor Bugbot for commit 9cff885. Configure here.
| // scripts run. | ||
| if (enforceContentSecurityPolicy) { | ||
| view.evaluateJavascript(contentSecurityPolicyMetaScript(), null) | ||
| } |
There was a problem hiding this comment.
Async CSP injection races page load
Medium Severity
CSP is installed via evaluateJavascript from onPageStarted, which runs asynchronously after navigation begins. Page HTML can parse and run scripts or start cross-origin subresource requests before the injected meta exists, so external network access may occur despite protocol_version isolation.
Reviewed by Cursor Bugbot for commit 9cff885. Configure here.
When a `web_view` declares a `protocol_version`, the renderer injects a fixed Content-Security-Policy meta tag at document start to isolate the content from external sources: only same-origin images/scripts/fonts are allowed, `data:` references for images/fonts are blocked, and XHR/fetch/WebSocket are disallowed (`connect-src 'none'`). Geolocation is disabled. The bundle must be fully self-contained. Recommended labels: pr:other, pr:RevenueCatUI, feat:Paywalls_V2
9cff885 to
9349754
Compare
📸 Snapshot Test1 modified, 592 unchanged
🛸 Powered by Emerge Tools |



Checklist
purchases-iosand hybridsMotivation
A hosted web bundle rendered inside the paywall must be isolated from external sources. For v1 the bundle is required to be fully self-contained, so the SDK enforces this rather than relying on the bundle to behave.
Part of PWENG-98.
Description
web_viewdeclares aprotocol_version, injects a fixed Content-Security-Policy<meta>at document start: same-origin scripts/images/fonts only, nodata:for images/fonts, and no XHR/fetch/WebSocket (connect-src 'none'). Geolocation is disabled.WebViewContentSecurityPolicyisinternalto:ui:revenuecatui(its only consumer is the renderer).WebViewContentSecurityPolicyTestandStyleFactoryTests.iOS parity: mirrors
purchases-ios(web-view-bridge-wiring).Stack (merge bottom-up): schema → rendering → CSP → value types → message model → variables provider → JS bridge → wiring.
Note
Medium Risk
Security-sensitive WebView behavior changes for v1 web bundles; CSP effectiveness depends on injection timing relative to page scripts, and legacy web_views without protocol_version remain unenforced.
Overview
Paywalls V2
web_viewcomponents that declareprotocol_versionare now treated as v1 self-contained bundles: the SDK enforces network isolation instead of trusting the hosted page.protocol_versionis threaded from the paywall schema throughStyleFactoryintoWebViewComponentStyle. When it is non-null,WebViewComponentViewturns on CSP enforcement; legacy configs withnullkeep today’s behavior (no injected policy).On load,
WebViewClient.onPageStartedrunscontentSecurityPolicyMetaScript()viaevaluateJavascriptso a fixed Content-Security-Policy meta is inserted at the top of<head>before page scripts run. The newWebViewContentSecurityPolicymodule defines that policy (same-origindefault-src,connect-src 'self',img-src/font-srcwithdata:, inline script allowances for the trusted bundle) plus escaping and idempotent install. WebView setup also disables geolocation.Coverage:
WebViewContentSecurityPolicyTestandStyleFactoryTestsforprotocolVersionpassthrough.Reviewed by Cursor Bugbot for commit 9349754. Bugbot is set up for automated code reviews on this repo. Configure here.