This crate aim to provide a type-safe way to handle and sanitize potentially untrusted values like user input.
It aims to provide compile-time Taint checking into Rust. All user input or in general all input coming from the outside world into the program must be seen as untrusted and potentially malicious (called tainted). A tainted value keeps its taint until a proper sanitization function is called upon the tainted data, clearing its taint.
This crate introduces several data types, traits and macros to simplify the process of taint tracking.
The goal of this crate is to help design more secure applications. By tainting all program inputs, unsanitized data can not be used by accident. By providing a sanitizing interface to tainted data, security analysis can focus on analysing the implemented sanitizing functions instead of identifying where tainted data is located, and where it is used.
User data must be wrapped within the container UntrustedValue
which
provides/marks the contained data as tainted.
use untrusted_value::{UntrustedValue, SanitizeWith};
let user_input: i32 = -36;
let user_input = UntrustedValue::from(user_input);
// user data cannot be used on accident, since it is contained inside UntrustedValues
// UntrustedValue does only provide a limited set of implemented traits like Clone
let user_input = user_input.sanitize_with(...) // removes the taint
When user data is a struct of different subtypes:
pub use untrusted_value::{IntoUntrustedVariant, SanitizeValue};
pub use untrusted_value::derive::UntrustedVariant;
#[derive(UntrustedVariant)]
#[untrusted_derive(SanitizeValueEnd, Clone)] // tainted variant of NetworkConfig should be Cloneable
pub struct NetworkConfig {
pub port: u32,
pub listen_address: String,
}
impl SanitizeValue<NetworkConfig> for NetworkConfigUntrusted {
type Error = // ...
fn sanitize_value(self) -> Result<NetworkConfig, Self::Error> { /* ... */ }
}
let user_data = load_from_config().to_untrusted_variant();
// user data cannot be used on accident, since it is contained inside UntrustedValues
let user_data = user_data.sanitize_value();
When a function is called by an application framework like Rocket/Poem/...,
the macro untrusted_inputs
may be used to taint the function inputs:
#[route(path = "/"), method = "get"]
#[untrusted_inputs]
fn index(name: &str) -> Result<String, ()> {
// MACRO inserts the following code:
// let name = UntrustedValue::from(name);
// let ... = UntrustedValue::from(...);
// we can not use "name" directly, since it is
// wrapped in an UntrustedValue
// we must explicitly sanitize the value before usage
let name = name.sanitize_with(/* func */)?;
Ok(format!("Hello, {}!", name))
}
A library providing a function that returns untrusted data may use the macro untrusted_output
to conditionally
taint the output if the library user desires this:
#[cfg_attr(feature = "some_feature", untrusted_output)]
pub fn query_database() -> String {
// if cfg matches, then use untrusted_output to wrap the
// function output in UntrustedValue
// the macro will wrap the body with:
// UntrustedValue::from(
"abcdef".to_string()
// )
}
See also the examples in the examples
directory.
The library is written in Rust, and can be added using cargo
:
cargo add untrusted-value
When using compile optimizations there should be no runtime overhead since
we are essentially just "renaming" data. The UntrustedValue
struct only contains a single field of the original data type.
When compiling for release the compiler should optimize all usage
of the UntrustedValue
struct away.
Enabled by default:
derive
: enables the macros to automatically generate code
Optional features:
derive_harden_sanitize
: enables hardening for the derive macroSanitizeValue
. When this feature is disabled, the implementedfn sanitize_value(self)
errors-early. Which may be undesired if sanitizing timing side channels are a concern. When enabling this feature, first all sanitizers are run, then the first error is propagated.
Providing a taint tracking system is nice but still requires the developer to taint the data properly. Currently, we are working on providing a crate level macro to automatically check common taint source like input from environment variables, args, and common frameworks, that will create a compile error if input data has not been tainted.
This crate does only provide an interface to taint and sanitize data. Using this system, still this does not make an application inherently secure. The developer must still implement appropriate sanitizing functions to clear the taint of the data. This unified interface should help to focus security analysis on the sanitizing functions instead of on potentially all places where tainted data might be used.
Contributions to the project are welcome! If you have a feature request, bug report, or want to contribute to the code, please open an issue or a pull request.
This project is licensed under the MIT license. See the LICENSE file for details.