// CWE-113: HTTP Response Splitting via CRLF Injection.
// Detects when user-controlled input is passed to response header setters
// without CRLF sanitization.
///|
priv struct Cwe113Visitor {
findings : Array[Finding]
file : String
source : String
severity : Severity
safe_vars : Map[String, Bool]
}
///|
pub fn cwe113_info() -> RuleInfo {
{
id: { cwe: 113, variant: "crlf-injection" },
name: "HTTP Response Splitting via CRLF Injection",
description: "Response header values set from user input without CRLF (\\r\\n) sanitization allow HTTP response splitting, enabling session fixation, XSS, and cache poisoning.",
default_severity: Error,
}
}
///|
pub fn run_cwe113(
impls : @list.List[@syntax.Impl],
file : String,
source : String,
findings : Array[Finding],
severity : Severity,
) -> Unit {
let visitor : Cwe113Visitor = {
findings,
file,
source,
severity,
safe_vars: {},
}
impls.each(fn(impl_) { @syntax.IterVisitor::visit_Impl(visitor, impl_) })
}
///|
fn is_header_setter(method_name : String) -> Bool {
method_name == "set_header" ||
method_name == "add_header" ||
method_name == "append_header" ||
method_name == "set_response_header"
}
///|
fn is_safe_for_header(env : Cwe113Visitor, expr : @syntax.Expr) -> Bool {
if is_safe_value(expr) {
return true
}
match expr {
Ident(id~, ..) => {
let name = match id.name {
Ident(name~) => name
Dot(id~, ..) => id
}
env.safe_vars.contains(name)
}
_ => false
}
}
///|
fn has_dynamic_header_value(
env : Cwe113Visitor,
args : @list.List[@syntax.Argument],
) -> Bool {
let mut pos_idx = 0
let mut result = false
args.each(fn(arg) {
match arg.kind {
Labelled(label) =>
if label.name == "value" && !is_safe_for_header(env, arg.value) {
result = true
}
LabelledPun(_) | LabelledOption(..) | LabelledOptionPun(..) => ()
_ => {
if pos_idx == 1 && !is_safe_for_header(env, arg.value) {
result = true
}
pos_idx += 1
}
}
})
result
}
///|
fn extract_pattern_name(pat : @syntax.Pattern) -> String? {
match pat {
Var(binder) => Some(binder.name)
Constraint(pat~, ..) => extract_pattern_name(pat)
_ => None
}
}
///|
impl @syntax.IterVisitor for Cwe113Visitor with fn visit_Expr_Let(
env,
pattern~,
expr~,
body~,
loc~,
) {
match extract_pattern_name(pattern) {
Some(name) =>
if is_safe_value(expr) || is_sanitized_expr(expr) {
env.safe_vars.set(name, true)
}
None => ()
}
@syntax.IterVisitor::base(env).visit_Expr_Let(pattern~, expr~, body~, loc~)
}
///|
impl @syntax.IterVisitor for Cwe113Visitor with fn visit_Expr_DotApply(
env,
self~,
method_name~,
args~,
return_self~,
loc~,
) {
if is_header_setter(method_name.name) && has_dynamic_header_value(env, args) {
env.findings.push(
make_finding(
{ cwe: 113, variant: "crlf-injection" },
env.severity,
Medium,
"Response header set with dynamic value without CRLF sanitization. Attacker can inject \\r\\n to split the HTTP response, enabling session fixation or XSS.",
env.file,
env.source,
loc,
),
)
}
@syntax.IterVisitor::base(env).visit_Expr_DotApply(
self~,
method_name~,
args~,
return_self~,
loc~,
)
}