Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created October 1, 2022 12:52
Show Gist options
  • Save bennadel/8cff18189c4bdb627e915a3de32f9f2a to your computer and use it in GitHub Desktop.
Save bennadel/8cff18189c4bdb627e915a3de32f9f2a to your computer and use it in GitHub Desktop.
Remediating CSV Injection Attacks 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 );
}
}
<cfscript>
serializer = new CsvSerializer();
rows = [
[ "ID", "NAME", "EMAIL" ],
[ 1, "Sarah ""Stubs"" Smith", "[email protected]" ],
[ 2, "John Johnson", "[email protected]" ],
[ 3, "=(3+5)", "#chr( 9 )#[email protected]" ], // <== CAUTION: Malicious data!
[ 4, "Jo Jamila", "[email protected]" ]
];
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment