Taint tracking
Data that comes from outside the program — command-line arguments, file contents, stdin — is data an attacker controls. In Halis it carries a taint label in the type.
tainted[str] is not a runtime wrapper. On the native backend it is a
compile-time property: the program cannot “forget” the label and still
compile. If you strip it without an explicit sanitising step, the build fails.
The three sources
Section titled “The three sources”| Source | What it yields |
|---|---|
tainted_args() |
Every command-line argument, as list[tainted[str]] |
read_file_tainted(path) |
The contents of a file that is not trusted |
read_line() |
One line from stdin |
The sinks
Section titled “The sinks”A sink is a place where data leaves the program and can do harm: print,
println, eprint, eprintln, read_file, write_file, file_exists,
exit, net_lookup, proc_exec, and the net_tcp_* / net_udp_send_to /
net_tls_get family.
A tainted value cannot reach a sink.
What the compiler says
Section titled “What the compiler says”error: taint-sink violation: println argument 1 is tainted[str](tainted values must be sanitised before reaching a sink — usesanitize_html / sanitize_path / sanitize_sql_identifier /sanitize_sql_string / sanitize_command / sanitize_filename fromstd.sanitize, or taint_unwrap() if you accept the risk)For a network sink the message names the actual threat:
error: taint-sink violation: net_tcp_connect argument 1 (host) is tainted[str](a tainted host enables SSRF + DNS rebinding — use sanitize_command,or taint_unwrap() if you accept the risk)Sanitising
Section titled “Sanitising”std.sanitize provides seven sanitiser families, each returning a clean
str: sanitize_html, sanitize_html_attr, sanitize_path,
sanitize_sql_identifier, sanitize_sql_string, sanitize_command and
sanitize_filename. All of them are pure — they declare no effect — so they
can be used anywhere.
import "std.sanitize"
fn main() -> int uses Args, IO { let argv: list[tainted[str]] = tainted_args() if argv.len() > 1 { println("hello, " + sanitize_filename(argv.get(1))) } return 0}Routing on tainted data without untainting
Section titled “Routing on tainted data without untainting”std.taint provides pure queries: taint_check_equals,
taint_check_starts_with, taint_check_contains, taint_check_len and
friends. They return a bool and keep the taint label on the other side.
if taint_check_equals(argv.get(1), "admin") { println("admin route")}You can route on untrusted data without stripping the label. The label is only dropped when the data actually leaves the program. This is the difference between a controlled taint system and a machine that simply deletes tags.
Honest limits
Section titled “Honest limits”- Taint is a single label.
tainted[str, Html]andtainted[str, Sql]are not implemented. Today, one sanitize call drops the label for every sink. taint_unwrap()is a deliberate escape hatch. Using it immediately disables the taint system for that value. That is the user’s decision, not a toolchain bug.
See the quick reference for the full table of sources and sinks.