Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the New York ColdFusion User Group (May. 2008) with: Michael Smith and Clark Valberg
Ben Nadel at the New York ColdFusion User Group (May. 2008) with: Michael Smith and Clark Valberg@clarkvalberg )

Using Hmac() And An Ordered Struct To Implement A Secure Email Tracking Pixel In Lucee 5.3.2.77

By Ben Nadel on
Tags: ColdFusion

Last week, I looked at using ordered, or linked, structs in Lucee 5.3.2.77. An ordered struct is one in which the keys will be iterated-over in a consistent, predictable manner. This predictable iteration is perfect for things like creating BSON documents in a MongoDB query. It's also perfect for creating secure hashes, where the order of the hashed inputs needs to be consistent across the producing and consuming contexts. An example of this would be creating a secure email tracking pixel in which the URL of the tracking pixel needs to be accessible by a non-authenticated client. In such a scenario, we can use an ordered struct and a hashed message authentication code (HMAC) to ensure that the secure URL was not tampered with in Lucee 5.3.2.77.

The goal of an email tracking pixel is track when a user has opened an email. This works by using a specialized application end-point as the src attribute of an img tag that is embedded within the email content. When the user opens the email, the email client attempts to load the embedded img, thereby calling the specialized application end-point and alerting the application to the fact that the user has opened the email.

NOTE: If the email client does not load images, the tracking pixel is rendered inactive. Also, if the email client proactively loads images into a cache ahead of time (as GMail used to do), it will no longer be an accurate representation of the user's activity.

Unlike a traditional request that a user would make against an application, the tracking pixel request is expected to be unauthenticated. Meaning, the request will not be inherently associated with any authenticated user Session. As such, the tracking pixel URL must contain an explicit reference to the user and to the action that is being tracked. Example:

/track/${ action }?userID=${ userID }

Because of this openly transparent way in which the data is being passed to the application server, it is important that we have a way to ensure that the URL is not manipulated for malicious intent. Such is the role of the request Signature. The request signature accounts for all of the values in the request, and provides a means where by the consumer can test the contents of the request against the signature.

And, this is where the ordered, or linked, Struct comes into play. By using an ordered struct when aggregating the request contents, it ensures that the composite message - that is ultimately authenticated - is generated in the same way by the both the Provider and the Consumer. If the two sides of the transaction were to generate the message in different ways, the resultant request signatures would not match and the request would be rejected.

By using an ordered struct, it simplifies the message composition. For example, here is a buildMessage() method that takes a parameters Struct. The algorithm assumes that the Struct is ordered, and therefore iterates over the keys without any additional ceremony:

/**
* I build the message (to be signed) based on the inputs.
* 
* @method I am the HTTP method that can be used to access to the resource.
* @resource I am the application resource being accessed.
* @parameters I am the set of key-value pairs associated with the request.
*/
private string function buildMessage(
	required string method,
	required string resource,
	required struct parameters
	) {

	var parts = [ method.lcase(), resource ];

	// We are assuming the parameters struct is an ORDERED STRUCT. As such, the order
	// of the keys in this iteration will be predictable and repeatable.
	for ( var key in parameters ) {

		parts.append( "#key.lcase()#=#parameters[ key ]#" );

	}

	return( parts.toList( "::" ) );

}

As you can see, the buildMessage() method takes the collection of values associated with a request and generates a string (to be signed). And, because the parameters argument is assumed to be ordered, it means that the for-in loop will always produce a consistent output based on a consistent input. It's this consistency that allows both sides of the transaction to generate the same signature.

ASIDE: Without an ordered struct, you can still generate a consistent signature by - for example - sorting the parameter keys alphabetically before adding them to the message.

To see this in action, we can create a very simple pixel tracking implementation. First, let's create a ColdFusion component that encapsulates the hmac() call and the generation of the signature. I'm calling this component, UrlSigner.cfc. It's only role to generate and assert signatures:

component
	output = false
	hint = "I create and validate signatures for secure URLs."
	{

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

	/**
	* I validate that the given signature matches the signature generated from the given
	* set of inputs. If the signatures do not match, an error is thrown.
	* 
	* @secretKey I am the key being used to sign the inputs.
	* @method I am the HTTP method that can be used to access to the resource.
	* @resource I am the application resource being accessed.
	* @parameters I am the set of key-value pairs associated with the request.
	* @signature I am the user-provided signature being validating.
	*/
	public void function assertSignature(
		required string secretKey,
		required string method,
		required string resource,
		required struct parameters,
		required string signature
		) {

		if ( ! validateSignature( argumentCollection = arguments ) ) {

			throw( type = "SignatureMismatch" );

		}

	}


	/**
	* I return the signature generated from the given set of inputs.
	* 
	* @secretKey I am the key being used to sign the inputs.
	* @method I am the HTTP method that can be used to access to the resource.
	* @resource I am the application resource being accessed.
	* @parameters I am the set of key-value pairs associated with the request.
	*/
	public string function createSignature(
		required string secretKey,
		required string method,
		required string resource,
		required struct parameters
		) {

		var message = buildMessage( method, resource, parameters );
		var signature = hmac( message, secretKey, "hmacSha512" );

		// NOTE: All hashed values are returned in HEX format. In order to make the URL
		// a bit shorter (for purely aesthetic reasons), let's convert from HEX format to
		// Base64url format.
		return( hexToBase64url( signature ) );

	}


	/**
	* I validate that the given signature matches the signature generated from the given
	* set of inputs.
	* 
	* @secretKey I am the key being used to sign the inputs.
	* @method I am the HTTP method that can be used to access to the resource.
	* @resource I am the application resource being accessed.
	* @parameters I am the set of key-value pairs associated with the request.
	* @signature I am the user-provided signature being validating.
	*/
	public boolean function validateSignature(
		required string secretKey,
		required string method,
		required string resource,
		required struct parameters,
		required string signature
		) {

		var expectedSignature = createSignature( secretKey, method, resource, parameters );

		return( ! compare( signature, expectedSignature ) );

	}

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

	/**
	* I build the message (to be signed) based on the inputs.
	* 
	* @method I am the HTTP method that can be used to access to the resource.
	* @resource I am the application resource being accessed.
	* @parameters I am the set of key-value pairs associated with the request.
	*/
	private string function buildMessage(
		required string method,
		required string resource,
		required struct parameters
		) {

		var parts = [ method.lcase(), resource ];

		// We are assuming the parameters struct is an ORDERED STRUCT. As such, the order
		// of the keys in this iteration will be predictable and repeatable.
		for ( var key in parameters ) {

			parts.append( "#key.lcase()#=#parameters[ key ]#" );

		}

		return( parts.toList( "::" ) );

	}


	/**
	* I convert the HEX format to Base64url format.
	* 
	* @input I am the hex input being formatted.
	*/
	private string function hexToBase64url( required string input ) {

		var bytes = binaryDecode( input, "hex" );
		var encodedOutput = binaryEncode( bytes, "base64" );

		// Replace the characters that are not allowed in the base64url format. The
		// characters [+, /, =] are removed for URL-based base64 values because they
		// have significant meaning in the context of URL paths and query-strings.
		var formattedOutput = encodedOutput
			.replace( "+", "-", "all" )
			.replace( "/", "_", "all" )
			.replace( "=", "", "all" )
		;

		return( formattedOutput );

	}

}

As you can see, this ColdFusion component is really just a glorified call to the hmac() function that is geared towards requests-based signature generation.

Now, let's create a simple ColdFusion template that sends out an Email that contains the tracking pixel. As mentioned above, the src attribute for the tracking pixel is really a request to our application. And, it will contain the following:

  • userID
  • emailType
  • signature

The role of the signature is to ensure that the userID and the emailType have not been tampered with; and, to ensure that the requested end-point is the expected end-point. For the sake of the demo, I'm just hard-coding the secret key being used to signed the request; but, let's assume that in a production application this would either be a user-specific value or an environment-specific value:

<cfscript>

	userID = 4;
	userSecretKey = "iCanHaz$ecretKey!:D";
	emailType = "important-thing";

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

	// Since the tracking pixel end-point will need to be accessible as part of a non-
	// authenticated request, we have to include a signature as part of the request so 
	// that we can ensure that the request was not tampered with. In doing so, we can
	// safely accept a predetermined number of URL parameters.
	signature = new UrlSigner().createSignature(
		secretKey = userSecretKey,
		method = "get",
		resource = "track.cfm",
		// NOTE: Using an ORDERED STRUCT to ensure that the parameters are always applied
		// to the signature in a predictable order.
		parameters = [
			emailType: emailType,
			userID: userID
		]
	);

	// Notice that the tracking URL now contains all the predetermined parameters AND the
	// securely generated signature.
	trackingUrl = (
		"http://127.0.0.1:56809/testing-lucee/pixel/track.cfm?" &
		"userID=#encodeForUrl( userID )#&" &
		"emailType=#encodeForUrl( emailType )#&" & 
		"signature=#encodeForUrl( signature )#"
	);

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

	// For the purposes of this demo, we're going to embed the image right in the SENDING
	// page. This way, it be loaded implicitly.
	echo( "<img src='#encodeForHtmlAttribute( trackingUrl )#' width='1' height='1' />" );

	// My email provider won't load the tracking pixel from my LOCAL LUCEE SERVER since
	// the image is being proxied by a remote mail server that won't have access to my
	// local server. As such, I am commenting-out the CFMail tag for the demo and just
	// using the embedded IMG tag above to show the full life-cycle.
	/*
	cfmail (
		to = "lucee@bennadel.com",
		from = "ben@bennadel.com",
		subject = "You have an important communication",
		type = "html"
		) {

		echo( "<p>Things are very important!</p>" );
		echo( "<img src='#encodeForHtmlAttribute( trackingUrl )#' width='1' height='1' />" );

	}
	*/

</cfscript>

As you can see, I'm using the various parts of the request to generate the signature; then, I'm appending the signature to the request itself. Locally, I don't have a mail server running; so, instead of using the cfmail tag, I'm simulating the email client by outputting the tracking pixel right in the response.

Now, if we run this Lucee CFML code in the browser, we get the following output:

A tracking pixel makes a signed request to the ColdFusion server from the mail client.

And, if we look more closely at the request parameters, we can see that the request signature is appended to the outgoing data-points:

The tracking pixel contains the request signature.

On the consuming side - in the tracking end-point - we can then take userID and emailType, re-generate the signature, and compare it so the one provided in the request:

<cfscript>
	
	// Ensure that the required URL parameters exist.
	param name = "url.userID" type = "string";
	param name = "url.emailType" type = "string";
	param name = "url.signature" type = "string";

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

	inputEtag = cgi.http_if_none_match;
	outputEtag = hash( "#cgi.request_method#/#cgi.script_name#?#cgi.query_string#" );

	// If the incoming ETag is present, it means that the client already requested this
	// tracking pixel end-point. As such, we can just return a 304 Not Modified and bail-
	// out of the template so that we can skip the overhead of having to process the
	// tracking action again.
	if ( inputEtag == outputEtag ) {

		cfheader (
			statuscode = 304,
			statustext = "Not Modified"
		);
		exit;

	}

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

	userSecretKey = "iCanHaz$ecretKey!:D";

	// In order to ensure that the request parameters have not been tampered with, we
	// need to compare the incoming signature to the one that we would have generated
	// with the predetermined parameters.
	// --
	// NOTE: The .assertSignature() method will THROW an error if the provided signature
	// does not match the expected signature.
	new UrlSigner().assertSignature(
		secretKey = userSecretKey,
		method = cgi.request_method,
		resource = "track.cfm",
		// NOTE: Using an ORDERED STRUCT to ensure that the parameters are always applied
		// to the signature in a predictable order.
		parameters = [
			emailType: url.emailType,
			userID: url.userID
		],
		signature = url.signature
	);

	systemOutput( "****************************************", true );
	systemOutput( "Signature Approved - Tracking Request!", true );
	systemOutput( "// --> userID : #url.userID#", true );
	systemOutput( "// --> emailType : #url.emailType#", true );
	systemOutput( "****************************************", true );

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

	// For the 1x1 pixel GIF, we're just going to use a Base64-encoded in-memory value.
	// This way, we don't have to read anything from disk.
	trackingPixelBinary = binaryDecode(
		"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
		"base64"
	);

	// Since we only care about the first viewing of the tracking pixel, let's set up
	// expiration headers as we send down with the tracking pixel GIF.
	nowAt = now();
	expiresAt = nowAt.add( "d", 1 );

	cfheader (
		name = "ETag",
		value = outputEtag
	);
	cfheader (
		name = "Expires",
		value = getHttpTimeString( expiresAt )
	);
	cfheader (
		name = "Cache-Control"
		value = "max-age: #nowAt.diff( 's', expiresAt )#, public"
	);
	cfheader (
		name = "Content-Disposition",
		value = "inline" // Or, attachment
	);
	cfcontent (
		type = "image/gif",
		variable = trackingPixelBinary
	);

</cfscript>

As you can see, on the consuming side of the tracking pixel, we take the request data and run it through the UrlSigner.cfc ColdFusion component just like we did on the producer side. Only this time, we're not generating the signature, we're validating it. And, if the signature doesn't match, it means that the request was tampered-with and the data must be assumed to be malicious.

In this demo, I don't have much of a tracking end-point - I'm just logging the request to the standard output:

The tracking pixel contains the request signature.

Once the email pixel tracking has been committed, I'm returning a valid 1x1 GIF image so that the embedded img tag doesn't show as broken. And, as an optimization, I'm providing standard HTTP caching and ETag headers so that I can short-circuit out of any subsequent requests to the same end-point.

Ordered Struct vs. Unordered Struct

I started off this post as an exploration of ordered, or linked, structs in Lucee 5.3.2.77. One case for ordered structs is in the consistent generation of an Hmac hash. And, one case for an Hmac hash is in the creation of email tracking pixels. That said, using an ordered struct for this use-case is not an unequivocal win. By using an ordered struct, both the producer and the consumer now have to agree upon the ordering of said ordered struct. This creates coupling in a way that is not immediately obvious when looking at the ColdFusion code. As such, it would probably be a better strategy to use an unordered struct and then sort the keys alphabetically within the UrlSigner.cfc.

That said, this was just a fun exploration of Lucee CFML 5.3.2.77 features in the context of a tracking pixel. I should say that I am not a security expert; so, don't take this approach as the guaranteed best way to secure the tracking pixel consumer. But, hopefully this was interesting enough.



Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
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.