Code injection attack is a method of taking advantage of a flaw where an application can be tricked to relay malicious code from a system to another system which then executes it. Common examples of such flaws are SQL injections and cross-site scripting (XSS) attacks. In OWASP Top 10 Most Critical Web Application Security Risks injection flaws take the pole position on the list. Apparently this thing is serious!
Static typing in programming languages can be easily used to make such attacks difficult or even impossible. For a successful SQL injection attack our application needs to handle strings: Attacker gives us a malicious string which we then somewhere in depths of our application use to construct a SQL query.
How to make things harder for an attacker? We can “stop using” strings. Or strictly speaking, isolate the string management and let the type system and compiler help us to verify the program is correct.
Here is how to do that in Rust. The example code is Rust specific, but anything mentioned here can be achieved in other programming languages. We define our own data type which is validated to contain only valid data. Then we can use this type for safe computation. Rust allows us to hide the insides of a type so we can ensure that our type is constructed only with validated input.
As an example we build a code to handle a common thing in software: usernames. First we define a (tuple) struct called a
Username which inside itself contains a string (Rust Strings are UTF-8 and verified to be if not meddled with unsafe constructors):
This struct is just a wrapping for a string inside it (this is called “newtype pattern”). However, since the string inside a
Username is not made public, it’s impossible to access it. The beef of this essay is making sure that we can create and use only
Username values that contain validated data.
Now to go forward we need a validator for usernames. The validator function below is kept as simple as possible for clarity. Here we only validate that the given username is of right length and only contain the characters our specification requires:
And now we need a way to construct an
Here I only implement the
new()method for the
Username and leave building other utility methods as an exercise for the reader.
So far it has been easy. Things don’t get much more complicated, but require some effort.
Every single place were a username gets input into the application (be it from user’s input, or from database, or from RPC call, etc.) we have to construct a
Username. And every single function where username is used should take a
Username instead of a String as an argument. This ensures that our application only uses validated usernames everywhere.
A good compiler helps with this step. When function’s type signature is updated, the compiler will give errors from places where the function is called with wrong arguments. Rust has been a prime example for really helpful error messages and inspired other compiler projects to follow
It’s easy to see the benefits of this method when comparing same function with two different type signatures (first is strongly typed and second is stringly typed):
create_user() takes specific types (assume that
Email are validated and constructed same way as
Username) it’s impossible to call it with wrong arguments. You cannot put email address where real name should be. It’s also not necessary to validate the inputs here because validation is already done when the type is constructed. This function can do what is its job: To create the user.
It’s not only about Strings
This method also works with other types also! This not only helps protecting against code injection attacks, but help keep things where they belong. For example your use case might have different id numbers:
ProductId. Compiler will help you keep your ID numbers in order if every different ID is also a different type. Or if geographic coordinates are thing to care about:
Latitude. Valid longitudes are between -180° and +180° and latitudes between -90° and +90°.