When is a guid not a guid?

This started with an email Sahad NK over in Azure Security about an unexpected behaviour when comparing Guid types in .NET. I had thought was widely understood, but a quick search through GitHub suggested otherwise. Because the pattern can lead to security bugs, it is worth explaining clearly.

Before getting to GUIDs, it helps to look at how components in a system interact. Components exchange data, and whether that data is treated as trusted depends on the contract between them. That contract may be explicit in documentation or implicit in behavior, but in either case it defines the assumptions each side makes. Because trust is contextual rather than absolute, misunderstandings between components can lead to incorrect or unsafe behavior.

Comparing Values

Take dates as an example. Different systems may represent the same date in different formats. The string 03/04/2026 can mean 3 April 2026 or March 4, 2026 depending on the agreed format. If both sides parse the value using the same contract, they still end up with equivalent DateTime values even though the original strings looked different.

Strings have a similar problem. A comparison that looks safe can still produce the wrong result if it depends on culture-specific casing rules. The Turkish-I case is a classic example.

string userInput = "interesting";

// canconicalize the string before comparison
bool comparison = "INTERESTING" == userInput.ToUpper();

Console.WriteLine(comparison);

A simple C# string comparison

This will always write true, unless you're in Turkey.

Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
string userInput = "interesting";

// canconicalize the string before comparison
bool comparison = "INTERESTING" == userInput.ToUpper();

Console.WriteLine(comparison);

A simple C# string comparison, which also sets the current thread culture to Turkish

The comparison will now return false, in English (and most other languages) the conversion of i to uppercase will be I. In Turkey the conversion of i to uppercase is İ, the dotted I. This is one reason why comparing things like permissions or security groups should not be done between strings, and why .NET has code analysis rules that will remind you not to do a compare without specifying what culture rules to use, and/or canonicalising both values in a comparison operation.

Comparing GUIDs

A GUID is a 128-bit identifier designed to be globally unique. In practice, you can think of it as a value whose job is identity rather than display: two GUIDs are either the same identifier or they are not, regardless of how their string forms are written.

If you're a SharePoint user you'll be aware of Microsoft's love for GUIDs, SharePoint URIs seem designed to use up all the permissible GUIDs, accelerating the heat death of the universe by using up entropy at an accelerated rate. If you're an Active Directory administrator you'll know that everything in AD has an associated GUID. If you've had me reviewing your authorization code you'll know that you should be using GUIDs to refer to user accounts, groups, permission sets and so on. They are everywhere, and they're so handy that the internet took was a Microsoft thing and standardised it as a UUID.

A GUID can be represented in several ways: as 16 bytes in memory, as a 128-bit value, or as a string that encodes that value. In .NET, the Guid struct handles these representations for you. The problem appears when a GUID moves between systems as text, because multiple string formats can represent the same underlying value.

For historical reasons, a GUID can appear in several string formats. It may include braces or omit them, include hyphens or omit them, and use uppercase or lowercase letters. .NET accepts all of these as valid input and parses them to the same Guid value.

Guid guidWithHypens = Guid.Parse("1d73dda0-0b00-479d-b47b-eb5622385dc2");
Guid guidWithBraces = Guid.Parse("{1d73dda0-0b00-479d-b47b-eb5622385dc2}");
Guid guidNoHyphens = Guid.Parse("1d73dda00b00479db47beb5622385dc2");
			
Console.WriteLine(guidWithHypens == guidWithBraces);
Console.WriteLine(guidWithHypens == guidNoHyphens);
Console.WriteLine(guidWithBraces == guidNoHyphens);

C# code comparing parsed GUIDs

If you run the code sample above all the comparisons will return true as you'd expect. However, remember I suggested GUIDs get passed around as strings, well picture an HTTP Request that has a form parameter with GUID in and the value has been extracted to a string.

Guid guidWithBraces = Guid.Parse("{1d73dda0-0b00-479d-b47b-eb5622385dc2}");
string requestGuidValue = "{1d73dda0-0b00-479d-b47b-eb5622385dc2}";
		
Console.WriteLine(guidWithHypens.Equals(requestGuidValue));

Using .Equals on a Guid to compare against a string

This returns false even though the parsed value would be identical. In Guid's case it doesn't have an implicit conversion to or from a string, using == will give you a syntax error. Some classes will do have implicit conversions and be forgiving in parsing from a string but give a canonical representation when converting to a string. Comparisons between an instance of such a class and a string may give incorrect results.

If this kind of comparison is used in authorization or filtering code, it can create confident but incorrect results. That can, as Sahed wrote, bypass deny lists, fragment cache or storage entries, produce inconsistent audit logs, and in some cases contribute to unintended privilege escalation.

While using a deny list instead of an allow list is a whole other security problem that code reviews and threat models should catch, using .Equals or == between different types may slip through. It compiles, so it must be correct right?

Fixing the problem

The safest rule is simple: compare values only after both sides have been converted to the same type. Compare int to int, Guid to Guid and so on. That way you are comparing the value itself rather than one of its possible textual representations.

Subscribe to Ramblings from a .NET Security PM

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe