Closed deatondg closed 3 years ago
I am currently working through the source code to try and implement this myself. So far, I have confirmed that simply deleting the Content-Security-Policy header on this domain allows my script to run.
Placing this block of code
# Only support downloaded scripts
if not useInline:
# There is only work to do if a CSP is present.
if "Content-Security-Policy" in flow.response.headers:
# Convert the CSP string to a dictionary using comprehension.
csp_dict = { f[0]: f[1] for f in [ f.strip().split(' ',1) for f in response.headers["Content-Security-Policy"].split(';') ] }
if "script-src" in csp_dict:
# If a script-src is present, we are going to add to it, so add a space
csp_dict["script-src"] += " "
else:
if "default-src" in csp_dict:
# If there is no script-src, but there is a default-src, we don't want to break the site, so copy the default-src into the script-src
csp_dict["script-src"] = csp_dict["default-src"] + " "
else:
# Otherwise, we can just set the script-src to be empty.
csp_dict["script-src"] = ""
# Add our URL (exists because not useInline) to the script-src
csp_dict["script-src"] += script.downloadURL
# Convert the dictionary back to a string using format strings and list comprehension
csp_string = ' ; '.join([f'{key} {value}' for key, value in csp_dict.items()])
# Update the CSP header to our new value
response.headers["Content-Security-Policy"] = csp_string
in the if isApplicable(script)
block in src/injector.py
adds the downloadURL of non-inline scripts into the script-src
field of the CSP. I thought about supporting inline scripts as well, but I didn't want to bother figuring out how to compute the required hashes. This works according to my very limited testing, but the code could be much better (this could be run once per request instead of once per script), so I don't feel comfortable making a PR yet.
If there is any demand, I could clean this up and make a PR.
Edit: Replaced the CSP string -> dict conversion to be more idiomatic. It's still fairly unreadable.
Interesting problem, I must say! It's always fun to see Userscript Proxy being used in actual real-world scenarios.
So far, I have confirmed that simply deleting the Content-Security-Policy header on this domain allows my script to run.
Could you perhaps share the CSP header value (appropriately censored if need be) to aid in reproducing the problem?
It's always fun to try out a new project like this! :) You've made something really awesome.
The domain I am visiting has no CSP, and the domain of the iframe has CSP
default-src 'self'; frame-src 'self' ; img-src 'self' ; connect-src 'self'
.
The snippet I provided modifies the iframe CSP to default-src 'self' ; frame-src 'self' ; img-src 'self' ; connect-src 'self' ; script-src 'self' https://path.to/my/script.js
which allows my script to run.
Alright! Great problem description all the way through! I can reproduce the problem locally now. Notably, I don't think it matters whether an iframe is involved or not.
With a CSP-configured web server up and running, I went on and made this change in Userscript Proxy so I could use the included test userscript for testing:
diff --git a/default-userscripts/userscript-proxy-test.user.js b/default-userscripts/userscript-proxy-test.user.js
index 8c33a7d..8a2e294 100644
--- a/default-userscripts/userscript-proxy-test.user.js
+++ b/default-userscripts/userscript-proxy-test.user.js
@@ -1,8 +1,7 @@
// ==UserScript==
// @name Userscript Proxy Test
// @version 1.0.0
-// @match *://example.com/*
-// @match *://www.example.com/*
+// @match *://*/*
// @run-at document-start
// ==/UserScript==
Then I started Userscript Proxy in host networking mode (so it can connect to the web server):
docker build -t userscript-proxy . && docker run -t --rm --name userscript-proxy --network host userscript-proxy
I could then connect to my web server through Userscript Proxy:
$ curl -v --proxy localhost:8080 localhost
* Trying 127.0.0.1:8080...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
[…]
< HTTP/1.1 200 OK
< Content-Security-Policy: default-src 'self'; frame-src 'self' ; img-src 'self' ; connect-src 'self'
[…]
<
<!DOCTYPE html>
<html><head><title>CSP</title>
</head><body><script data-userscript-proxy-version="1.0.0">
// ==UserScript==
// @name Userscript Proxy Test
// @version 1.0.0
// @match *://*/*
// @run-at document-start
// ==/UserScript==
[…]
</script><h1>CSP</h1>
</body></html>
And sure enough, after setting localhost:8080
as proxy in Firefox, I got a CSP error in the console upon visiting http://192.168.1.212
(connections to localhost
aren't proxied by Firefox):
🛑 Content Security Policy: The page’s settings blocked the loading of a resource at inline (“default-src”).
Since I got this error with an inline-injected script, I believe we should be adding 'unsafe-inline'
to script-src
if useInline
is true. But maybe there are security implications we should consider …
Awesome, I'm glad you were able to reproduce it.
I'm not informed enough to know the full security implications of adding 'unsafe-inline'
, but I can imagine a scenario in which a website includes an inline script and relies on the CSP to prevent it from running, either by accident or as some security feature to make sure that the CSP isn't being ignored. I have never seen this behavior before though.
Either way, I think the maximally correct solution would be to either include the hash of the inline script or put a nonce on it (examples here) to make sure that only the user's script get injected, and not anything else that might be hanging around on the page. That shouldn't be too much extra work as we need to parse and modify the CSP with either solution.
Great digging! I think a nonce would be the easiest, fastest and cleanest solution.
I agree, I can't think of any new downsides that change would introduce.
Having tried to implement it for a few hours, I can: The userscript itself might try to load external resources, for example by inserting a <link rel="stylesheet" href="https://example.com/stylesheet.css" />
. That doesn't work:
🛑 Content Security Policy: The page’s settings blocked the loading of a resource at https://example.com/stylesheet.css
I'll need to have another look into this to see what can be done.
Hm... Good point. Perhaps this could be regarded as a feature instead of a bug?
Possible solutions could be to include new header field(s) in the userscript to add things to the CSP (when a CSP is present), to add the same type of thing to the rules file so that it's per-domain, or to have a similar setting as a launch argument to apply uniformly to all sites. Alternatively, there could be a flag to just totally delete the CSP on every site or just on select sites.
Yeah, there are definitely several options to choose from. I have experimented and arrived at a somewhat reasonable solution; will create a PR ASAP.
My particular use case requires running a user script inside an iframe on my iPad. Unfortunately, it is not a publicly accessible domain, so I cannot share it here.
Attempting to inject my script fails with
[Error] Refused to execute a script because its hash, its nonce, or 'unsafe-inline' appears in neither the script-src directive nor the default-src directive of the Content Security Policy. (auth, line 33)
once the iframe is included, but I am able to see the script present in the HTML for the iframe.It seems to me that since this proxy is able to arbitrarily modify the presented HTML, it would be possible to compute the hash of our injected script and add it to the CSP before presenting the page, thus avoiding this issue.