Best practice CSP for `file_server browse`

The standard browse template uses inline JavaScript and CSS. Chrome has started to warn against this unless a Content Security Policy is set to unsafe-inline, nonce or hash. Using script-src 'self' is no longer accepted. Content Security Policy  |  Privacy & Security  |  Chrome for Developers

I’ve split the template and instead included the JavaScript and CSS as files, however there are still inline script and styles applied inside tags, so I still need unsafe-inline. One example is caddy/modules/caddyhttp/fileserver/browse.html at c2ccf8690f315aa0ebab930c3aadcc6cd11fc9e9 · caddyserver/caddy · GitHub

My question is what would be the best method to solve this? Is using unsafe-inline a real threat vector if the site is HTTPS only?

We could also use a nonce or hash. In this case we need to use a variable in the template, and set the variable value in the Caddyfile. The corresponding CSP header must share the same value. Hardcoding a nonce or hash could be feasible in my personal setup, though it is probably not wise in a generic template.

I’ve put this question in general as I would like to hear your thoughts on this topic. Perhaps later some changes might come out from it.

I suppose it is “cleaner” to separate the JS into <script> tags, but HTML tag attributes are so convenient for generated markup because we don’t have to worry about event listeners, propagation, etc.

My understanding is that unsafe-inline is unsafe if a third party can inject markup into the DOM and cause it to be executed as JS. If the application is safely escaping injected content, or using .innerText instead of .innerHTML and that sort of thing, then there shouldn’t be an issue.

In the case of our standard browse template, I think the only attack vector is someone placing files on the part of the file system being browsed, that can somehow be interpreted as HTML/JS and executed as such.

I think the risk is quite low, but at some point we should probably either extract the inline JS and CSS into script and style tags, or at least triple-check that we’re sanitizing inputs properly.

Pull requests welcomed :smiley:

2 Likes

I have slightly modified the browse template because I wanted to have a CSP as well. My CSP looks like this:

default-src 'none'; img-src 'self' https://cdn.example.com; object-src 'none'; base-uri 'none'; script-src 'strict-dynamic' 'nonce-6a1cb651-83bc-496a-94d0-fef81de5cb3d'; style-src 'strict-dynamic' 'nonce-6a1cb651-83bc-496a-94d0-fef81de5cb3d'; frame-ancestors 'self'; form-action 'self'; block-all-mixed-content;

which is rated by Google CSP evaluator like this:

The CSP Nonce is created like this in the first lines of the browse.html:

{{ $NONCE := uuidv4 -}}
{{ $NONCE_SCRIPT := print "nonce=" (quote $NONCE) -}}
{{ $NONCE_STYLE := $NONCE_SCRIPT -}}
{{ $CSP := printf "default-src 'none'; img-src 'self' https://cdn.example.com; object-src 'none'; base-uri 'none'; script-src 'strict-dynamic' 'nonce-%s'; style-src 'strict-dynamic' 'nonce-%s'; frame-ancestors 'self'; form-action 'self'; block-all-mixed-content;" $NONCE $NONCE -}}
{{ .RespHeader.Set "Content-Security-Policy" $CSP -}}

The <style> were changed to <style {{ $NONCE_STYLE }}>.
And things like

  • <body onload="initPage()">
  • <a href="javascript:queryParam('layout', '')" id="layout-list" class='layout{{if eq $.Layout "list" ""}}current{{end}}'>
  • <input type="search" placeholder="Search" id="filter" onkeyup='filter()'>

have been changed to

  • <a href="#" id="layout-list" class='layout{{if eq $.Layout "list" ""}}current{{end}}'>

and / or moved

<script {{ $NONCE_SCRIPT }}>
...
			const filterElem = document.getElementById("filter");
			if (filterElem) {
				filterElem.addEventListener("keyup", filter);
			}

			document.getElementById("layout-list").addEventListener("click", function() {
				queryParam('layout', '');
			});
			document.getElementById("layout-grid").addEventListener("click", function() {
				queryParam('layout', 'grid');
			});
			window.addEventListener("load", initPage);
...

I can’t remember the details, but the biggest struggle was the svg for the caddy-logo. Eventually, I moved the svg content to an external file, placed it on our “cdn” subdomain.

I am not sure, if all of these changes makes sense in a PR for all Caddy users using the default browse template because of how the files within a directory listing are displayed. I would be quite sure that new issues would be raised like, “the CSP is preventing my video objects to be shown in grid layout” …

2 Likes

That’s interesting. Do you mean Caddy emits the nonce generated in browse.html templateas a HTTP header? If so, then I’d say it probably would fit good as a general template as users can use browse without having to configure any CSP.

Yes, my custom browse.html generates the nonce, uses the nonce for any <style> and <script> and sends it as Response Header using the templates function .RespHeader.Set .

Give me some time to think about the difficulties I’ve had with the embedded svg for the Caddy logo and then I can submit my proposal as a pull request. I think it would be possible to add a subdirective to browse to disable the default content-security-policy header to have a solution available when Caddy users are very unhappy with such content-security-policy.

1 Like

The problem with the embedded svg for the “Served with caddy” can be seen here: https://alma.stbu.net/
Just open the Developer-Tools and see the issues raised such as:

Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'strict-dynamic' 'nonce-d48bab01-d7d5-4715-94cf-16fad91b9aea'". Either the 'unsafe-inline' keyword, a hash ('sha256-8U4DTM3ZAtnhPIZVOm/BvfynsvX7/vdspJ0hRqKCIdo='), or a nonce ('nonce-...') is required to enable inline execution. Note that hashes do not apply to event handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword is present.

That refers to this line in the browse.html
<svg class="caddy-logo" viewBox="0 0 379 114" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;">

I don’t see a Content-Security-Policy (and privacy) friendly way unless:

  • the caddy-logo <svg>...</svg> content is moved to a file and referenced in the HTML such as <img class="caddy-logo" src="/caddy-logo.svg" alt="Caddy logo"> => see question to Matt below.
  • add many hashes to the CSP image directive such as sha256-8U4DTM3ZAtnhPIZVOm/BvfynsvX7/vdspJ0hRqKCIdo=
  • remove the styles from the <svg> such as the colors (compare the nice, colored, original caddy-logo svg appearance here https://alma.stbu.net/without-csp/ and with the removed stlyes due to the Content-Security-Policy not allowing them: https://alma.stbu.net/)

@matt , would it be possible to “embed” the caddy-logo svg into the caddy binary and maybe serve it from an url such as /.well-known/caddy-logo.svg handled “by magic” just like /.well-known/acme-challenge?
If that would be possible, this would allow the first mentioned alternative.

Update:
I think I have found a good solution by moving inline svg styles to a a separate style, where the nonce will be used. After a couple more tests I will start to work on the PR.

1 Like

The pull request is this:

The proposed, changed browse.html is active here: : https://alma.stbu.net/testing-something/

Thanks! :smiley:

Google PageSpeed recommend adding unsafe-inline, http and https CSP to be compatible with older browsers. What do you think about that?

1 Like

That makes sense :+1:

These values for backward compatibility with older browsers have been added.

https://csp-evaluator.withgoogle.com/ also suggested these, and now it looks better:

2 Likes

Thanks for your work on this! I’ll review it shortly :slight_smile:

2 Likes

@matt Have you had any luck with this?

My default CSP is highly restrictive. I’ve tried modifying my Caddyfile as below, but it doesn’t appear to work.

(standards) {
    encode zstd gzip
    file_server
    header {
        Referrer-Policy "no-referrer"
        Content-Security-Policy "default-src 'none'; manifest-src 'self'; script-src 'self'; connect-src 'self'; style-src 'self'; media-src 'self'; font-src 'self'; img-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';"
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        Cache-Control "max-age=31536000"
        X-XSS-Protection "1; mode=block"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "deny"
        -Last-Modified
        -Server
    }
}

# HTML Domain

mycaddydomain.com, www.mycaddydomain.com {
    root * /var/www/mycaddydomain
    import standards
    header /browsedir/* {
        Content-Security-Policy "default-src 'none'; manifest-src 'self'; script-src 'self', 'unsafe-inline'; connect-src 'self'; style-src 'self', 'unsafe-inline'; media-src 'self'; font-src 'self'; img-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';"
    }
    handle /browsedir/* {
		file_server browse
    }
    handle_errors {
		redir https://mycaddydomain.com
    }
}

EDIT: I had to remove my CSP from my “standards” insert entirely, then add the following to my site. This seems to work, and the rest of my site is protected.

    route {
	header Content-Security-Policy "default-src 'none'; manifest-src 'self'; script-src 'self'; connect-src 'self'; style-src 'self'; media-src 'self'; font-src 'self'; img-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';"
	header /browsedir/* Content-Security-Policy "default-src 'none'; manifest-src 'self'; script-src 'self', 'unsafe-inline'; connect-src 'self'; style-src 'self', 'unsafe-inline'; media-src 'self'; font-src 'self'; img-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';"
    }

What do you mean exactly? We merged the PR, if that’s what you’re asking.

1 Like

Sorry, I just mean that using the file_server browse directive appears to still require a slightly less strict CSP (with Caddy v2.8.4). It would be great if this functionality would work using a strict CSP such as the statement below:

Content-Security-Policy "default-src 'none'; script-src 'self'; connect-src 'self'; style-src 'self'; media-src 'self'; font-src 'self'; img-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';"

From a security perspective, and for peace of mind, I want all of my sites to generate an A+ score at https://securityheaders.com

The PR to have a Content-Security-Policy in the file_server browse was merged on July 6th and is not part of Caddy v2.8.4 (Released June 2nd).
You might want to try the v2.9.0-beta.3.

This is how the Content-Security-Policy from Caddy v2.9.0-beta.3 browse will be rated on Securityheaders.com:

That was my temporary (already reverted) Caddyfile for above test:

todo.stbu.net {
        header {
                Permissions-Policy "geolocation=()"
                Referrer-Policy "strict-origin-when-cross-origin"
                X-Content-Type-Options "nosniff"
                Strict-Transport-Security "max-age=31536000"
        }

        file_server {
                root /home/stbu/caddy-prod/browse-test/test-dir
                browse
        }
}
1 Like