Gem #82: Type-Based Security 1: Handling Tainted Data

by Yannick Moy —AdaCore

Let's get started…


The notions of tainted data and trusted data usually refer to data coming from the user vs. data coming from the application. Tainting is viral, in that any result of a computation where one of the operands is tainted becomes tainted too.

Various C/C++ static analyzers provide checkers for tainted data that help find bugs where data from the user serves to compute the size of an allocation, so that an attacker could use this to trigger a buffer overflow leading to an Elevation of Privilege (EoP) attack.

In Ada, the compiler can provide the guarantee that no such bugs have been introduced by accident (although you can still bypass the rule if you really want to, for example by using Unchecked_Conversion or address clause overlays), provided different types are used for tainted and trusted data, with no run-time penalty. This can be done with many types of data, including basic types like integers.

Let's say tainted data is of an integer type. The basic idea is to derive the trusted type from the tainted one, and to provide a function Value to get to the raw data inside a trusted value, like the following:

package Taint is

   type Trusted_Value is new Integer;

   function Value (V : Trusted_Value) return Integer;
   pragma Inline(Value);

end Taint;

Notice that the implementation of Value is just a type conversion:

package body Taint is

   function Value (V : Trusted_Value) return Integer is
   begin
      return Integer (V);
   end Value;

end Taint;

Then, make sure the sensitive program uses trusted data:

with Taint; use Taint;

procedure Sensitive (X : Trusted_Value) is
begin
   null; --  Do something sensitive with value X
end Sensitive;

Let's try to pass in data from the user to the sensitive program:

with Taint;
with Sensitive;

procedure Bad (Some_Value : Integer) is
begin
   Sensitive (Some_Value);
end Bad;

The compiler returns with a type error:

bad.adb:6:15: expected type "Trusted_Value" defined at taint.ads:3
bad.adb:6:15: found type "Standard.Integer"

Now, this does not prevent us from doing useful computations on trusted data as easily as on tainted data, including initialization with literals, case statements, array indexing, etc.

with Taint; use Taint;
with Sensitive;

procedure Good is
   Max_Value : constant := 100;
   X : Trusted_Value := Max_Value;
begin
   X := X + 1; --  Perform any computations on X
   Sensitive (X);
end Good;

Because Trusted_Value is a type derived from the tainted type (Integer), all operations allowed on tainted data are also allowed on trusted data, but operations mixing them are not allowed.

Be aware that nothing prevents the program itself from converting between tainted data and trusted data freely, but this requires inserting an explicit conversion, which can be spotted during code reviews.

To completely prevent such unintended conversions (say, to facilitate maintenance), the type used for trusted data must be made private, so that only the package which defines it can convert to and from it. With Trusted_Value being private, we should also provide a corresponding function for each literal which we used previously, as well as the operations that we'd like to allow on trusted values (note that for efficiency all operations could be inlined):

package Taint is

   type Trusted_Value is private;

   function Value (V : Trusted_Value) return Integer;

   function Trusted_1 return Trusted_Value;
   function Trusted_100 return Trusted_Value;
 
   function "+" (V, W : Trusted_Value) return Trusted_Value;

private

   type Trusted_Value is new Integer;

end Taint;

The new implementation is as expected:

package body Taint is

   function Value (V : Trusted_Value) return Integer is
   begin
      return Integer (V);
   end Value;
   
   function Trusted_1 return Trusted_Value is
   begin
      return 1;
   end Trusted_1;

   function Trusted_100 return Trusted_Value is
   begin
      return 100;
   end Trusted_100;

   function "+" (V, W : Trusted_Value) return Trusted_Value is
   begin
      return Trusted_Value (Integer (V) + Integer (W));
   end "+";

end Taint;

Of course, the client now needs to be adapted to this new interface:

with Taint; use Taint;
with Sensitive;

procedure Good is
   X : Trusted_Value := Trusted_100;
begin
   X := X + Trusted_1; --  Perform any computations on X
   Sensitive (X);
end Good;

That's it! No errors can result in tainted data being accidentally passed by the user where trusted data is expected, and future maintainers of the code won't be tempted to insert conversions when the compiler complains.


About the Author

Yannick Moy’s work focuses on software source code analysis, mostly to detect bugs or verify safety/security properties. Yannick previously worked for PolySpace (now The MathWorks) where he started the project C++ Verifier. He then joined INRIA Research Labs/Orange Labs in France to carry out a PhD on automatic modular static safety checking for C programs. Yannick joined AdaCore in 2009, after a short internship at Microsoft Research.

Yannick holds an engineering degree from the Ecole Polytechnique, an MSc from Stanford University and a PhD from Université Paris-Sud. He is a Siebel Scholar.