When it comes to security, it’s better to learn from examples.
This episode explains a very important and simple piece of advice. Always test for things that you want instead of things you don’t want.
You’ll need to understand the differences between an Ordinal string comparison vs. OrdinalIgnoreCase vs. InvariantCulture when deciding if filenames are what you think they are.
Listen to the episode or read the full transcript below.
Failure is a good thing when we learn from it. In fact, if we’re not failing, then we’re not trying new things. And because a lot of programming will be new to us, we can expect to fail a lot. Especially when we’re just getting started with programming.
Now a normal failure might mean that your new design doesn’t work the way you expect. But some failures can be taken advantage of to cause something unexpected in your code. Or at least, unexpected to you. For the person taking advantage of the error, the behavior is exactly what an attacker wants.
There’s a whole class of attacks that take advantage of the filesystem. These attacks can be caused by mistakes in how your code uses the filesystem. Under normal usage, you might never notice. Until, for example, an attacker sets up a few files with some special names.
Let’s say that you need to write some code that uses the filesystem. And you’re worried that somebody could try to misuse your program in ways that should not be allowed. So you put checks in your code to prevent this. You even test it and sure enough, your code catches the problem and stops the misuse.
The problem is that you’re still thinking like a regular user making mistakes. You need to be aware of different ways that an attacker can purposefully lead your application to do things you never expected would be possible.
This is where it’s good to learn from examples. There’s so many ways that it’s hard to count how your code can be misled and allow some misuse to proceed. You really don’t want to have to learn each of these attacks the hard way.
I’ll explain in this series of episodes some common mistakes that you can learn from. This episode will focus on filesystem name attacks. This is not a full security episode. There’s just too much for a single episode even after limiting the discussion to filesystems.
Okay, the first attack you can avoid with some simple advice. Always test for things that you want instead of things you don’t want.
Think of it like this. You have a path leading to your door that you want to make sure people walk on. And if somebody walks on the grass instead of the path, then you won’t answer the door. Seems simple enough right?
My advice in this situation is to watch the path. But for some reason we’re wired to want to watch the grass instead. We see somebody walking on the grass and then the doorbell rings and we refuse to answer the door.
That’s the normal behavior we build into our applications. And it works fine until a thief sneaks around the side of the house and under the window that we’re using to watch the grass. We hear the doorbell and think it must be okay because we didn’t see anybody walking on the grass.
The really interesting part of this example though is that sometimes we still don’t understand and find more ways to avoid watching the path. Maybe we think that our grass watching abilities are limited so we make the window bigger and put up lights on the grass so nobody can sneak past.
And it works. Until a thief climbs a tree, jumps onto the roof, and then drops down in front of the door and rings the bell again.
The point is, anytime you have things you want to check for, make sure to check for them directly. Don’t try to eliminate all the things you don’t want to happen. Focus on the things you do want to happen.
If you expect a file to have a certain name, then check the name directly and if it doesn’t match, then reject it. Don’t try to look for all the ways it doesn’t match because there will always be one more that you didn’t think of.
Now, that’s not the end of the story. Because file names are strings, you have to be very careful about how you compare things. The same guidance applies, look for what you want. But with strings, you need to be aware of locales.
Actually, you need to be aware of locales for more than just strings. Dates and numbers have their own problems. Right now, I’m focusing on strings.
A locale allows different rules to be associated with world regions and countries. You know how other countries have different ways of doing things. And I’m not talking about which side of the road we drive on. We all have normal ways to write numbers, normal ways to express dates and times, and it’s not just our languages that are different. Our languages have different rules for how strings should be compared and sorted.
Some filesystems are case insensitive. What does that mean? It means that if you name a file abc.txt using all lowercase letters, then you can open the file later with the name abc.txt using all uppercase letters. Or maybe only abc.txt with just the first letter a as uppercase. Or maybe just the txt extension is uppercase. The possibilities are endless.
Well, they’re actually not endless, but there can be a lot of them. I actually like case sensitive filesystems but maybe that’s because I also like to program in C++.
So to help users out, we sometimes relax the rules a bit. Most users probably won’t even be aware of case. And some might have their caps lock permanently turned on so everything they type looks like they’re yelling.
All of this goes against the advice of having one thing that you look for and rejecting all others. When it comes to names, we normally want to offer a little more flexibility. And that’s when the trouble begins.
There’s all kinds of ways this can be exploited. One in particular is called the Turkish I problem. I don’t know but it always seemed to me that Turkey has more consistent rules for the letter I than the English language. Just think for a moment about the letter I in English. The lowercase version has a dot. And the uppercase version has no dot. It’s something that we normally never think about. It just is.
Well, in Turkey, this inconsistency has been resolved. By creating two letter I’s. One I has a dot and one I doesn’t. I’m not talking about how English has a dot for lowercase and no dot for uppercase. In Turkey, there’s a lowercase I with a dot, an uppercase I with a dot, and a lowercase I without a dot and an uppercase I without a dot. They actually have four I’s instead of two!
And the rules are clear. If you start out with a lowercase I with a dot and make it uppercase, then you get the uppercase I also with a dot. In other words, the dot stays in place. There’s none of this switching between a lowercase with a dot and then an uppercase without a dot.
Why is all this important to filesystems? Because file names are likely to contain the letter I. Even the word file itself has an I in it.
Now you might think that it’s not a big deal. That maybe you don’t have many customers using your software in Turkey. But you probably also don’t have many thieves sneaking across your roof when you’re busy watching the grass either.
You see, if there’s a way to exploit your code, then bad people will use it. And if it’s as simple as telling their computer to use a Turkish locale for a while, then they’re going to make use of that. You need to be ready.
When comparing strings, there are ways to compare strings that don’t simply use the current locale or don’t assume any specific locale. There’s also a locale that represents a set of common and unchanging rules called an InvariantCulture.
When you have something that you need to check for security purposes, the best option is called Ordinal. If you can lock down the case so you know for certain that a filename should always use all lowercase letters, for example, then a straight ordinal comparison looks only at the exact binary values that make up each letter.
If you want to allow some flexibility in the case rules, and still need the security, then use OrdinalIgnoreCase.
Whatever you decide, just remember that you should allow only what is expected instead of trying to prevent the unexpected. By definition, the unexpected is going to surprise you.
Then, once you know what to look for, make sure to look for it with either Ordinal or OrdinalIgnoreCase rules. Use the InvariantCulture or local cultures when you don’t have any security concerns and want the extra special rules that are defined in each country and region of the world.