Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Clark Valberg
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Clark Valberg ( @clarkvalberg )

Mitigating Cross-Site Scripting (XSS) Attacks With A Strict Content Security Policy (CSP) In ColdFusion 2021

By on
Tags:

As I continue to evolve my blogging platform, bringing it into the modern ColdFusion era, I'm trying to catch up on best practices. Of course, I've always used SQL query parameterization to block SQL injection attacks. And, I use encodeForHtml() and encodeForHtmlAttribute() in as many places as is feasible. And when converting user-provided markdown into HTML, I use the OWASP Anti-Samy project to sanitize the HTML output. But, one thing I've never had is a Content Security Policy (CSP). A CSP is yet another line-of-defense in the war against Cross-Site Scripting (XSS) attacks.

CAUTION: I Am Not A Security Expert

Let's be real clear here - I am not a security expert. But, the concept of security is increasingly shifting left in our industry. Meaning, security concerns are becoming everyone's concerns, not just that of the Security and/or Operations teams. As such, I'm trying to become a good citizen.

In order to get my Content Security Policy up and running, I just followed Google's guide on Strict CSPs. I also came across a CSP guide from Pete Freitag - one of the foremost security experts in our ColdFusion community.

In this post, I'm just sharing how I've implemented the CSP on this blog - hopefully I didn't screw anything up too bad.

My Strict Content Security Policy (CSP)

This blog has, basically, two pieces of content: the blog posts that I write; and, the comments that y'all write. And, while the vast, vast majority of you are amazing, beautiful people, some of you are garbage who want nothing more than to post malicious content. Thankfully, malicious content is all-but-curbed through my use of spam detection, Anti-Samy HTML sanitization, and proper content encoding. However, security is an ongoing arms race. And, hopefully, adding a Content Security Policy (CSP) is yet another weapon that I can use to help maintain the peace.

A Content Security Policy defines which resources your browser is allowed to load; and, which inline actions your browser is allowed to evaluate. In my implementation, I'm using an HTTP response header - Content-Security-Policy - and a nonce (N-once), which is a one-time-use token generated uniquely on each request. The nonce is supplied in the HTTP header and must correspond to a nonce attribute that is explicitly added to every <script> tag that is present on my blog.

Since the nonce value is unique for every page request, even if an attacker were able to inject a malicious script tag, the browser would refuse to evaluate it since it wouldn't contain a matching nonce attribute. In a "persisted XSS" attack, the malicious content is "static" and therefore could not possibly match the nonce which changes on every page request.

There are two parts to the Content Security Policy (CSP): the policy itself (delivered via HTTP header or <meta> tag) and the optional reporting of CSP violations to an API end-point. In an attempt to centralize the handling of all-things-CSP, I created a ContentSecurityPolicy.cfc ColdFusion component which generates the CSP response configuration and handles incoming policy violations.

And, again, what I have here is more-or-less what is outlined in Google's strict CSP guide:

component
	accessors = true
	output = false
	hint = "I provide methods for generating Content Security Policy (CSP) headers and logging violations."
	{

	// Define properties for dependency-injection.
	property config;
	property logger;
	property secureRandom;

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I return the Content Security Policy (CSP) configuration, including the inline
	* nonce and the relevant HTTP Headers.
	*/
	public struct function getCspConfig() {

		// When CSP violations are blocked by the browser, the browser will (eventually)
		// report those violations to this end-point.
		var reportToUrl = "#config.url#index.cfm?event=api.csp.report";
		// The nonce (N-once) is a unique value that is generated fresh on for page
		// request. It is then included in the CSP header and must match "nonce"
		// attributes in scripts tags as a means denote the script tags as trusted. You
		// can think if it kind of like a reverse CSRF-Token.
		var nonce = secureRandom.getEncodedBytes( 16, "base64" );

		var directives = [
			// Configure how script tags are allow-listed. "nonce-{token}" requires
			// script tags to include the "nonce" attribute with the given value.
			// "strict-dynamic" allows already-trusted scripts to programmatically load
			// additional scripts without supplying a nonce attribute. "https:" is a
			// fallback for Safari, which doesn't support "strict-dynamic".
			// "unsafe-inline" is a fallback for really old browsers.
			// --
			// NOTE: The browsers are smart enough to ignore the older, looser script
			// tag directives if they also support "strict-dynamic". As such, the
			// presence of these does not reduce the strength of the policy.
			"script-src 'nonce-#nonce#' 'strict-dynamic' https: 'unsafe-inline'",
			// Deny all object media loads (such as Flash/SWF plugins).
			"object-src 'none'",
			// Block injection of <base> tags, which a malicious actor could use to
			// change the loading of relative-path script tags.
			"base-uri 'none'",
			// Report CSP violations to the the following end-point. Older browsers use
			// the report-uri directive while newer browsers use the Report-To header.
			"report-uri #reportToUrl#",
			"report-to csp-endpoint"
		];

		// NOTE: While the Report-To HTTP header is technically broader in scope than the
		// Content-Security-Policy HTTP header, I have no other use for it at this time.
		// As such, I'm defining it as part of the CSP configuration (especially since
		// the two headers have to agree on the reporting group). This may change in the
		// future if I ever need an additional reporting end-point.
		return({
			nonce: nonce,
			header: {
				name: "Content-Security-Policy",
				value: directives.toList( "; " )
			},
			reportToHeader: {
				name: "Report-To",
				value: serializeJson({
					group: "csp-endpoint",
					max_age: 10886400,
					endpoints: [
						{
							"url": reportToUrl
						}
					]
				})
			}
		});

	}


	/**
	* I log Content Security Policy violations (as reported by the browser).
	*/
	public void function logReport( required string content ) {

		// Ignore noisy end-points (I know these are getting blocked).
		if ( content.reFindNoCase( "/resources/jing/[^.]+\.swf" ) ) {

			return;

		}

		logger.info(
			"CSP violation.",
			{
				content: content
			}
		);

	}

}

The getCspConfig() method returns the nonce and the two HTTP headers that are relevant to the CSP. The nonce is generated using 16 random bytes (128 bits) which is being supplied via the SHA1PRNG algorithm in my SecureRandom.cfc ColdFusion component:

component
	output = false
	hint = "I define methods for securely generating random values."
	{

	/**
	* I return a cryptographically secure random byte.
	*/
	public numeric function getByte() {

		// NOTE: In Java, bytes are signed. Which means that the left-most bit (of the
		// byte) indicates the sign of the byte. In order to not jump through hoops to
		// convert a random INT into a SIGNED BYTE, we'll just produce an INT in the
		// range that we know will fit nicely into a byte. We're still exercising all
		// 8-bits.
		return( javaCast( "byte", getInt( -128, 127 ) ) );

	}


	/**
	* I return a cryptographically secure random binary value with the given length (ie,
	* a byte array).
	*/
	public binary function getBytes( required numeric byteCount ) {

		var bytes =  arrayNew( 1 )
			.resize( byteCount )
		;

		for ( var i = 1 ; i <= byteCount ; i++ ) {

			bytes[ i ] = getByte();

		}

		return( javaCast( "byte[]", bytes ) );

	}


	/**
	* I return a cryptographically secure random binary value with the given length
	* encoded using the given encoding.
	*/
	public string function getEncodedBytes(
		required numeric byteCount,
		string encoding = "hex"
		) {

		return( binaryEncode( getBytes( byteCount ), encoding ) );

	}


	/**
	* I return a cryptographically secure random integer between the two values,
	* inclusive. This method lays the groundwork for the rest of the component methods.
	*/
	public numeric function getInt(
		required numeric minValue,
		required numeric maxValue
		) {

		return( randRange( minValue, maxValue, "SHA1PRNG" ) );

	}


	/**
	* I return a cryptographically secure random token of the given length. The token
	* is composed of URL-friendly characters.
	*/
	public string function getToken( required numeric tokenLength ) {

		// The set of character inputs that we can compose the random token.
		var charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
		var charsetCount = charset.len();

		var letters =  arrayNew( 1 )
			.resize( tokenLength )
		;

		for ( var i = 1 ; i <= tokenLength ; i++ ) {

			letters[ i ] = charset[ getInt( 1, charsetCount ) ];

		}

		return( letters.toList( "" ) );

	}

}

With this CSP configuration in-tow, I then generate it at the top of every single request that travels through the onRequestStart() event-handler in my ColdFusion application framework component (truncated for demo):

component {

	public void function onRequestStart() {

		// ... truncated for demo.

		request.template = {
			type: "standard",
			statusCode: 200,
			statusText: "OK",
			csp: contentSecurityPolicy.getCspConfig()
		};

		// ... truncated for demo.

	}

}

This request.template.csp configuration then comes into play in two places:

  • I have to render the two HTTP headers at the top of every template.
  • I have to inject the unique nonce into every single <script> tag.

The HTTP header injection uses the CFHeader tag and looks like this:

<cfscript>

	// Set the strict Content-Security-Policy.
	cfheader( attributeCollection = request.template.csp.header );
	cfheader( attributeCollection = request.template.csp.reportToHeader );

</cfscript>

Notice that I'm using the attributeCollection syntax instead of defining the name and value attributes individually. At first, I messed this up because I tried using the argumentCollection syntax. However, even though cfheader() looks like a Function invocation, it's actually just the script-based syntax for tags - which have attributes, not arguments.

The nonce attribute injection then looks like this:

<script
	type="text/javascript"
	src="/linked/dist/js/main.js"
	async
	nonce="#encodeForHtmlAttribute( request.template.csp.nonce )#">
</script>

And, again, I had to add this nonce to every single script tag across all my templates! It was a little tedious; but, my site isn't actually that large so it only took a few minutes.

Now, when the browser detects CSP violations, it will use the Report-To HTTP header to figure out where - if at all - it should report those issues. My API end-point for reporting CSP violations is super simple, though it did require a bit of trial-and-error:

<cfscript>

	httpRequestData = getHttpRequestData();
	// Once I added the logging for CSP violations, they were all showing up as
	// byte-arrays. I'm not sure why, but the browser(s) seem to be posting the
	// reports as binary encodings. As such, I'm encoding the binary into a
	// UTF-8 charset if it arrives as a byte-array.
	content = isBinary( httpRequestData.content )
		? charsetEncode( httpRequestData.content, "utf-8" )
		: httpRequestData.content
	;

	application.contentSecurityPolicy.logReport( content );

	request.template.content = {
		ok: true
	};

</cfscript>

... which just turns around and logs it to Rollbar.

Logging of CSP violations ends-up being pretty noisy because the browser doesn't just apply the Content Security Policy to the content that I deliver - it also applies it to browser extensions. As such, I'm getting a lot of reports that make no sense (and cannot be reproduced when I visit the afflicted URLs). And, of course, with my object-src 'none' directive, I'm blocking all of my old Flash videos that I used to record for demos. But, since no one has Flash plugins installed anyway, this CSP directive isn't really "making it worse".

So, that's how I implemented the Content Security Policy (CSP) on my blog. Hopefully I haven't broken anything inadvertently. And, there are many more directives than the ones I have here. But, as with all things security, it's a balancing act between security, level-of-effort, and user experience (UX).

Want to use code from this post? Check out the license.

Reader Comments

11 Comments

I sympathise with evaluating the logs its a pain, they are pretty cryptic and like you say its hard to separate out plugins, I think sometimes folks have things in their browsers that are not quite what they think they are.

I found this securityheaders.com helpful when i was trying to evaluate how secure a banks site i was working on was theres alot of different headers. It gave me a pointer towards some others I could apply.

I find it reassuring if a site i check has it all covered my bank does not ;)

You can sign resources like static scripts with a with an integrity attribute I never got to that because of how the site was built. That might be a less resource intensive way of reassuring that a script for example is authentic, because you would only do it once. Not sure how much support that has.

Ps: nice server name in your headers :)

15,674 Comments

@Adam,

Yeah, the nonce attribute is good for static assets; but, as with the bank site, it's not always that easy. Even I ran into issues because a lot of my old blog posts have embedded Object-tags in the actual post content (for when I used to use SWF videos instead of Vimeo for my recordings). If I wanted to add a nonce attribute to those embedded objects, I'd have to parse the blog-content as I was rendering it and then dynamically inject the attribute. Could I do it? Yes. Would it be a pain in the behind? Yes.

Like I said above, security is all about striking a balance.

11 Comments

Yes, things are never as neat as you would like them to be.
Security is a bit of a wormhole, its interesting though.

15,674 Comments

@All,

Soooo, I've turned off the reporting of CSP violations. It's just too noisy, and it's going to put me over the free tier on my current logging service (Rollbar). If I start paying for logging, I'll turn it back on; but, for the time being, I'm just going to ignore them.

81 Comments

Here's how I've been generating the nonce (0ms).

nonce = toBase64(generateSecretKey("AES"));

Is this less secure, random or compatible than using your SecureRandom CFC?

Tool-wise, I benefited from using "chrome-csp-disable" browser extension while integrating CSP into our first couple of projects. (I still keep it installed in case I want to override CSP on 3rdParty websites to use a bookmarklet.)
https://github.com/PhilGrayson/chrome-csp-disable

15,674 Comments

@James,

Honestly, I don't even know what makes a "securely random" value "secure" 😨 So, I can't speak to the security of it. I also wonder how relevant it is in this case. Because, the nonce only represents a moment in time, the moment an attacker would try to persist something, it would seemingly at most, only be potentially dangerous if another user then rendered that persisted XSS value at exactly the right moment when your server were to generate a nonce that matched the "guessed nonce". And, I mean, is that ever going to happen? Seems ludicrously unlikely.

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel