Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Andy Matthews
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Andy Matthews@commadelimited )

Remediating CSV Injection Attacks In ColdFusion

By on
Tags:

A few days ago, I didn't know what a CSV Injection Attack was. I love generating CSV (Comma Separated Value) files in ColdFusion. And, heretofore, I had always thought of CSV files as containing nothing more than inert text data. On Wednesday, however, David Epler - one of our senior security engineers - got the results of our ongoing Penetration Test (PenTest); and, lo-and-behold, one of the identified vulnerabilities was "CSV Injection". Since this was an unknown attack vector for me, I assume it is also an unknown issue for some of you. As such, I wanted to look at remediating CSV Injection attacks in ColdFusion.

As with all of the OWASP (Open Web Application Security Project) Injection attacks, CSV Injection allows raw, user-provided data to trigger unintended actions in the target system. With CSV Injection, the unintended action is the evaluation of a Formula in the context of spreadsheet program (such as MS Excel, Google Sheets, Numbers) on the victim's computer.

For example, if a CSV data file contains the encoded field:

"=3+2"

... the presence of the equal sign (=) as the first character may cause the given spreadsheet program to evaluate the cell content as a math equation, yielding the sum, 5.

Maths - while challenging for my Caveman brain - isn't in-and-of-itself a threat. But, as George Mauer points out in his article on CSV Injection, some spreadsheet programs have formulas that can execute low-level commands or even make HTTP requests to remote servers. Yikes!

The remediation of CSV Injection attacks recommended by OWASP is to:

  • Quote fields.
  • Escape embedded quotes.
  • Prepend the field value with a single-quote (') if the field starts with one of the malicious characters that can trigger a formula evaluation:
    • Equals (=)
    • Plus (+)
    • Minus (-)
    • At (@)
    • Tab (0x09)
    • Carriage return (0x0D)

Since I love writing ColdFusion components, my remediation for the CSV Injection outlined in the PenTest was to create a component that centralizes the logic for serializing Array data into CSV data. It only has two public methods:

  • serializeArray( rows )
  • serializeArrayAsBinary( rows )

The second method is just a convenience method since I'm almost always taking the generated CSV data and piping it into the variable attribute of the CFContent tag. This component builds on my previous post, celebrating the joys of CSV in ColdFusion:

component
	output = false
	hint = "I provide helper methods for SAFELY serializing Array data as CSV content."
	{

	// These are used internally, but can also be used externally as well in order to make
	// the calling code more obvious (seeing names is easier than seeing ASCII numbers).
	this.chars = {
		COMMA: ",",
		TAB: chr( 9 ),
		NEWLINE: chr( 10 ),
		CARRIAGE_RETURN: chr( 13 ),
		QUOTES: """",
		ESCAPED_QUOTES: """"""
	};

	/**
	* I initialize the CSV serializer with the given defaults.
	*/
	public void function init(
		string fieldDelimiter = this.chars.COMMA,
		string rowDelimiter = this.chars.NEWLINE,
		string encoding = "utf-8"
		) {

		variables.defaultFieldDelimiter = fieldDelimiter;
		variables.defaultRowDelimiter = rowDelimiter;
		variables.defaultEncoding = encoding;

	}

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

	/**
	* I serialize the given array data as a CSV string payload.
	*/
	public string function serializeArray(
		required array rows,
		string fieldDelimiter = defaultFieldDelimiter,
		string rowDelimiter = defaultRowDelimiter
		) {

		var csvData = rows
			.map(
				( row ) => {

					return( this.serializeArrayRow( row, fieldDelimiter ) );

				}
			)
			.toList( rowDelimiter )
		;

		return( csvData );

	}


	/**
	* I serialize the given array data as a CSV binary payload.
	* 
	* NOTE: This method is provided for convenience - when I generate CSV content, I am
	* ALMOST ALWAYS then streaming the content back to the client using CFContent, which
	* accepts a binary `variable` attribute.
	*/
	public string function serializeArrayAsBinary(
		required array rows,
		string fieldDelimiter = defaultFieldDelimiter,
		string rowDelimiter = defaultRowDelimiter,
		string encoding = defaultEncoding
		) {

		return(
			charsetDecode(
				serializeArray( rows, fieldDelimiter, rowDelimiter ),
				encoding
			)
		);

	}

	// ---
	// PRIVATE METHODS.
	// ---

	/**
	* I return a value in which any KNOWN potential CSV injection vector has been escaped.
	* 
	* CAUTION MODIFIES DATA OUTPUT: This works by checking the first character in the
	* field; and, if it is a potentially dangerous character, it prepends the field value
	* with a single-quote. Some spreadsheet programs will hide this single-quote; others
	* will render it. As such, it may appear to the end user that we've altered their data
	* (which we have). Unfortunately, there's no way around this.
	*/
	private string function escapeCsvInjection( required string field ) {

		switch ( field.left( 1 ) ) {
			// These characters are mentioned on the OWASP website.
			// --
			// https://owasp.org/www-community/attacks/CSV_Injection
			case "=":
			case "+":
			case "-":
			case "@":
			case this.chars.TAB:
			case this.chars.CARRIAGE_RETURN:
			// The pipe character was mentioned in our PenTest results, not on OWASP.
			case "|":

				return( "'" & field );

			break;
			default:

				return( field );

			break;
		}

	}


	/**
	* I return a field value that is quoted and in which embedded special characters have
	* been escaped.
	*/
	private string function escapeField( required string field ) {

		return(
			this.chars.QUOTES &
			field.replace( this.chars.QUOTES, this.chars.ESCAPED_QUOTES, "all" ) &
			this.chars.QUOTES
		);

	}


	/**
	* I serialize the given row, both QUOTING and ESCAPING the content of each field.
	*/
	private string function serializeArrayRow(
		required array row,
		required string fieldDelimiter
		) {

		var csvData = row
			.map(
				( field ) => {

					// NOTE: Calling the toString() method to cast each field to a string
					// in case we are dealing with non-string simple values. This allows
					// us to call member-methods in all of the subsequent invocations.
					var escapedValue = toString( field );
					escapedValue = escapeCsvInjection( escapedValue );
					escapedValue = escapeField( escapedValue );

					return( escapedValue );

				}
			)
			.toList( fieldDelimiter )
		;

		return( csvData );

	}

}

The root of the solution here is that every field value is passed through these three methods:

  • toString()
  • escapeCsvInjection()
  • escapeField()

What I end up with is fields that show up looking like this:

=cmd | Sarah "Stubs" Smith

... and end up getting serialized like this:

"'=cmd | Sarah ""Stubs"" Smith"

Now, let's try to generate a CSV file in ColdFusion that contains malicious content. Notice that I'm using the convenience method, serializeArrayAsBinary(), to pipe the data directly to the CFContent tag. This resets the output buffer and halts all processing on the server:

NOTE: This code is running in Lucee CFML. As such, it does not need the cf prefixes on the CFScript-based tags.

<cfscript>

	serializer = new CsvSerializer();

	rows = [
		[ "ID", "NAME", "EMAIL" ],
		[ 1, "Sarah ""Stubs"" Smith", "sarah.smith@example.com" ],
		[ 2, "John Johnson", "jon.johnson@example.com" ],
		[ 3, "=(3+5)", "#chr( 9 )#dr.evil@example.com" ], // <== CAUTION: Malicious data!
		[ 4, "Jo Jamila", "jo.jamila@example.com" ]
	];

	header
		name = "content-disposition"
		value = getContentDisposition( "user-data.csv" )
	;
	content
		type = "text/csv; charset=utf-8"
		variable = serializer.serializeArrayAsBinary( rows )
	;

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I get the content-disposition header value for the given filename.
	*/
	private string function getContentDisposition(
		required string filename,
		string disposition = "attachment"
		) {

		var encodedFilename = encodeForUrl( filename );

		return( "#disposition#; filename=""#encodedFilename#""; filename*=UTF-8''#encodedFilename#" );

	}

</cfscript>

If we then run this ColdFusion code, generate the CSV file, and import it into Google Sheets, we get the following output:

A malicious CSV file imported into Google Sheets show remediation applied in ColdFusion.

As you can see, the the malicious field values have been neutralized with the single-quote prefix. Furthermore, Google Sheets doesn't render the quote in the sheet output; but, if you look in the Formula input, you can see it.

I am continually impressed with how people find new and interesting ways to misuse and abuse technologies. Thankfully, SQL Injection has long since been all but eradicated from ColdFusion thanks to the CFQueryParam tag. And, many forms of persisted XSS (Cross-Side Scripting) attacks have been neutralized by the various encodeForXYZ() methods. Perhaps we need to introduce an encodeForCsv() method?

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

Reader Comments

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

Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.