Created
October 1, 2022 12:52
-
-
Save bennadel/8cff18189c4bdb627e915a3de32f9f2a to your computer and use it in GitHub Desktop.
Remediating CSV Injection Attacks In ColdFusion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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