Articles

hashid / ishashidValid UDFs

Protect against sequential traversal when using INT keys

Image about hashid / ishashidValid UDFs
April 19, 2025

The hashid and ishashidValid CFML user-defined functions (UDFs) provide a simple mechanism for generating and validating hash-based identifiers. It's useful for creating somewhat unique and verifiable IDs, especially in scenarios where you want to avoid exposing the underlying sequential or predictable nature of simple integer IDs. The optional extra parameter adds a layer of security by allowing you to "salt" the hash, making it harder to guess or reverse-engineer the IDs without knowing the salt. The delimiter option allows the hashes to be adaptable to your environment and hash requirements in case dashes are not a good fit.

Consecutive Integers are a Security Risk for Publicly Accessible Resources

  • Predictability: The most obvious risk is that consecutive integers are easily predictable. An attacker who knows one valid ID can likely guess others by simply incrementing or decrementing the number.

  • Enumeration: This predictability allows for easy enumeration or scraping of resources. Automated tools can be used to systematically go through a range of IDs to access a large number of files or records.

  • Lack of Obscurity: Unlike random or unique identifiers (like UUIDs), sequential IDs offer no inherent obscurity, making it straightforward to discover and access resources if access controls are weak or non-existent.

  • Potential for Information Disclosure: If these predictable IDs are used to access resources containing sensitive information, the consequences of a breach can be severe, as seen in the examples above.

Examples of vulnerabilities

Parler (2021): The social media platform Parler used sequential integer IDs for its posts. By simply iterating through the post IDs, they could download virtually all public posts, including associated metadata like geocached photos and videos. This resulted in a massive archive of user data being collected.

Panera Bread (2018-2019): Security researchers discovered that Panera Bread's website exposed customer records through sequential integer IDs in their URLs. Anyone could potentially access the personal information of other customers, including names, email addresses, physical addresses, phone numbers, birthdays, and even the last four digits of credit card numbers in some cases.

Fiserv (2019): A security researcher reported a vulnerability in a web application provided by Fiserv, a major financial technology services provider. The application allegedly used sequential IDs to access sensitive financial documents.

Source Code

<cfscript>
public string function hashid(string id="", string extra="", string delimiter="-") output=false hint="Generates a hashid string using an integer (w/optional extra salt)" {
	arguments.delimiter = (listfind("-,_,;,:,.,^,$,!", arguments.delimiter)) ? arguments.delimiter : "-";
	return trim(arguments.id) & arguments.delimiter & hash("#trim(arguments.id)##trim(arguments.extra)#", "MD5");
}

public boolean function ishashidValid(string id="", string extra="", string delimiter="-") output=false hint="Determines if hashid is valid (w/optional extra salt)" {
	arguments.delimiter = (listfind("-,_,;,:,.,^,$,!", arguments.delimiter)) ? arguments.delimiter : "-";
	return javacast("boolean", (listlen(arguments.id, arguments.delimiter) eq 2 && find(arguments.id, hashid(listfirst(arguments.id,arguments.delimiter), trim(arguments.extra), arguments.delimiter))));
}
</cfscript>

Demo

<cfscript>
// hashid and ishashidValid unit tests

tests = [];

testHash = hashid("1");
arrayappend(tests, [
	"testName": "simple integer test (w/o secret)"
	,"originalValue": "1"
	,"hash": testHash
	,"expectation": "YES/TRUE"
	,"result": ishashidValid(id=testHash)
]);

testHash = hashid("1", "this-is-a-secret");
arrayappend(tests, [
	"testName": "simple integer test (w/secret)"
	,"originalValue": "1"
	,"hash": testHash
	,"expectation": "YES/TRUE"
	,"result": ishashidValid(id=testHash, extra="this-is-a-secret")
]);

testHash = hashid("abc123");
arrayappend(tests, [
	"testName": "text string (w/o secret)"
	,"originalValue": "abc123"
	,"hash": testHash
	,"expectation": "YES/TRUE"
	,"result": ishashidValid(id=testHash)
]);

testHash = hashid("abc123").replaceAll("-", "_");
arrayappend(tests, [
	"testName": "text string with modified delimiter (w/o secret)"
	,"originalValue": "abc123"
	,"hash": testHash
	,"expectation": "NO/FALSE (expected '-' as the delimiter)"
	,"result": ishashidValid(id=testHash)
]);

testHash = hashid("xyz987", "this-is-a-secret");
arrayappend(tests, [
	"testName": "text string (w/secret)"
	,"originalValue": "xyz987"
	,"hash": testHash
	,"expectation": "YES/TRUE (the hash & secret match)"
	,"result": ishashidValid(id=testHash, extra="this-is-a-secret")
]);

testHash = hashid("xyz987");
arrayappend(tests, [
	"testName": "text string (w/o secret, but requires one when decoding)"
	,"originalValue": "xyz987"
	,"hash": testHash
	,"expectation": "NO/FALSE (the secret doesn't match)"
	,"result": ishashidValid(id=testHash, extra="this-is-a-secret")
]);

testHash = hashid("xyz987", "this-is-a-secret", ":");
arrayappend(tests, [
	"testName": "text string (w/secret and modified delimiter)"
	,"originalValue": "xyz987"
	,"hash": testHash
	,"expectation": "YES/TRUE"
	,"result": ishashidValid(id=testHash, extra="this-is-a-secret", delimiter=":")
]);

testHash = hashid("xyz987", "this-is-a-secret");
arrayappend(tests, [
	"testName": "text string (w/secret and mismatched delimiter)"
	,"originalValue": "xyz987"
	,"hash": testHash
	,"expectation": "NO/FALSE (delimiter doesn't match)"
	,"result": ishashidValid(id=testHash, extra="this-is-a-secret", delimiter=":")
]);

writedump(var=tests, label="Test Results");
</cfscript>