use crate::labels::{Cap, DataLabel, Kind, LabelRule, ParamConfig, RuntimeLabelRule};
use crate::utils::project::{DetectedFramework, FrameworkContext};
use phf::{Map, phf_map};
pub static RULES: &[LabelRule] = &[
LabelRule {
matchers: &["ENV", "gets"],
label: DataLabel::Source(Cap::all()),
case_sensitive: false,
},
LabelRule {
matchers: &["params"],
label: DataLabel::Source(Cap::all()),
case_sensitive: false,
},
LabelRule {
matchers: &[
"request.headers",
"request.body",
"request.url",
"request.referrer",
"request.path",
],
label: DataLabel::Source(Cap::all()),
case_sensitive: false,
},
LabelRule {
matchers: &["request.cookies", "request.session", "cookies", "session"],
label: DataLabel::Source(Cap::all()),
case_sensitive: false,
},
LabelRule {
matchers: &["CGI.escapeHTML", "ERB::Util.html_escape"],
label: DataLabel::Sanitizer(Cap::HTML_ESCAPE),
case_sensitive: false,
},
LabelRule {
matchers: &[
"CGI.escape",
"Rack::Utils.escape_html",
"sanitize",
"strip_tags",
],
label: DataLabel::Sanitizer(Cap::HTML_ESCAPE),
case_sensitive: false,
},
LabelRule {
matchers: &["Shellwords.escape", "Shellwords.shellescape"],
label: DataLabel::Sanitizer(Cap::SHELL_ESCAPE),
case_sensitive: false,
},
LabelRule {
matchers: &["to_i", "to_f"],
label: DataLabel::Sanitizer(Cap::all()),
case_sensitive: false,
},
LabelRule {
matchers: &["sanitize_sql", "sanitize_sql_array"],
label: DataLabel::Sanitizer(Cap::SQL_QUERY),
case_sensitive: false,
},
LabelRule {
matchers: &["URI.encode_www_form_component"],
label: DataLabel::Sanitizer(Cap::URL_ENCODE),
case_sensitive: false,
},
LabelRule {
matchers: &["system", "exec"],
label: DataLabel::Sink(Cap::SHELL_ESCAPE),
case_sensitive: false,
},
LabelRule {
matchers: &["=open"],
label: DataLabel::Sink(Cap::SHELL_ESCAPE),
case_sensitive: false,
},
LabelRule {
matchers: &["subshell"],
label: DataLabel::Sink(Cap::SHELL_ESCAPE),
case_sensitive: true,
},
LabelRule {
matchers: &["File.open", "File.new", "File.read", "IO.read"],
label: DataLabel::Sink(Cap::FILE_IO),
case_sensitive: false,
},
LabelRule {
matchers: &["eval"],
label: DataLabel::Sink(Cap::CODE_EXEC),
case_sensitive: false,
},
LabelRule {
matchers: &["puts", "print"],
label: DataLabel::Sink(Cap::HTML_ESCAPE),
case_sensitive: false,
},
LabelRule {
matchers: &[
"Net::HTTP.get",
"Net::HTTP.post",
"URI.open",
"OpenURI.open_uri",
"HTTParty.get",
"HTTParty.post",
],
label: DataLabel::Sink(Cap::SSRF),
case_sensitive: false,
},
LabelRule {
matchers: &["HttpClient.request", "HttpClient.get", "HttpClient.post"],
label: DataLabel::Sink(Cap::SSRF),
case_sensitive: false,
},
LabelRule {
matchers: &[
"Net::HTTP.post",
"RestClient.post",
"RestClient.put",
"RestClient.patch",
"Faraday.post",
"Faraday.put",
"Faraday.patch",
"HTTParty.post",
"HTTParty.put",
"HTTParty.patch",
"Typhoeus.post",
"Typhoeus.put",
"Typhoeus.patch",
],
label: DataLabel::Sink(Cap::DATA_EXFIL),
case_sensitive: false,
},
LabelRule {
matchers: &["HttpClient.post", "HttpClient.put", "HttpClient.patch"],
label: DataLabel::Sink(Cap::DATA_EXFIL),
case_sensitive: false,
},
LabelRule {
matchers: &["Marshal.load", "Marshal.restore", "YAML.load"],
label: DataLabel::Sink(Cap::DESERIALIZE),
case_sensitive: false,
},
LabelRule {
matchers: &["constantize", "safe_constantize"],
label: DataLabel::Sink(Cap::DESERIALIZE),
case_sensitive: false,
},
LabelRule {
matchers: &["find_by_sql", "connection.execute", "select_all"],
label: DataLabel::Sink(Cap::SQL_QUERY),
case_sensitive: false,
},
LabelRule {
matchers: &["where", "order", "group", "having", "joins", "pluck"],
label: DataLabel::Sink(Cap::SQL_QUERY),
case_sensitive: true,
},
LabelRule {
matchers: &["redirect_to"],
label: DataLabel::Sink(Cap::SSRF),
case_sensitive: false,
},
LabelRule {
matchers: &["send_file"],
label: DataLabel::Sink(Cap::FILE_IO),
case_sensitive: false,
},
LabelRule {
matchers: &["html_safe", "raw"],
label: DataLabel::Sink(Cap::HTML_ESCAPE),
case_sensitive: false,
},
];
pub static KINDS: Map<&'static str, Kind> = phf_map! {
"if" => Kind::If,
"unless" => Kind::If,
"while" => Kind::While,
"until" => Kind::While,
"for" => Kind::For,
"return" => Kind::Return,
"break" => Kind::Break,
"next" => Kind::Continue,
"program" => Kind::SourceFile,
"body_statement" => Kind::Block,
"do_block" => Kind::Function,
"then" => Kind::Block,
"else" => Kind::Block,
"elsif" => Kind::If,
"begin" => Kind::Try,
"rescue" => Kind::Block,
"ensure" => Kind::Block,
"case" => Kind::Block,
"when" => Kind::Block,
"class" => Kind::Block,
"module" => Kind::Block,
"do" => Kind::Block,
"block" => Kind::Function,
"call" => Kind::CallMethod,
"assignment" => Kind::Assignment,
"method" => Kind::Function,
"singleton_method" => Kind::Function,
"subshell" => Kind::CallFn,
"comment" => Kind::Trivia,
";" => Kind::Trivia, "," => Kind::Trivia,
"(" => Kind::Trivia, ")" => Kind::Trivia,
"\n" => Kind::Trivia,
};
pub static PARAM_CONFIG: ParamConfig = ParamConfig {
params_field: "parameters",
param_node_kinds: &["identifier"],
self_param_kinds: &[],
ident_fields: &["name"],
};
const AR_QUERY_METHOD_NAMES: &[&str] = &["where", "order", "group", "having", "joins", "pluck"];
const AR_QUERY_SAFE_ARG0_KINDS: &[&str] = &[
"pair",
"hash",
"simple_symbol",
"hash_key_symbol",
"array",
"string",
"string_literal",
];
pub fn ar_query_safe_shape(callee_text: &str, arg0_kind: &str, has_interpolation: bool) -> bool {
let leaf = callee_text.rsplit(['.', ':']).next().unwrap_or(callee_text);
if !AR_QUERY_METHOD_NAMES.contains(&leaf) {
return false;
}
if matches!(arg0_kind, "string" | "string_literal") && has_interpolation {
return false;
}
AR_QUERY_SAFE_ARG0_KINDS.contains(&arg0_kind)
}
pub fn framework_rules(ctx: &FrameworkContext) -> Vec<RuntimeLabelRule> {
let mut rules = Vec::new();
if ctx.has(DetectedFramework::Rails) {
rules.push(RuntimeLabelRule {
matchers: vec!["permit".into(), "require".into()],
label: DataLabel::Sanitizer(Cap::all()),
case_sensitive: false,
});
}
if ctx.has(DetectedFramework::Sinatra) {
rules.push(RuntimeLabelRule {
matchers: vec!["erb".into(), "haml".into()],
label: DataLabel::Sink(Cap::HTML_ESCAPE),
case_sensitive: false,
});
}
rules
}
#[cfg(test)]
mod ar_query_tests {
use super::ar_query_safe_shape;
#[test]
fn hash_form_is_safe() {
assert!(ar_query_safe_shape("Model.where", "pair", false));
assert!(ar_query_safe_shape("where", "pair", false));
}
#[test]
fn symbol_form_is_safe() {
assert!(ar_query_safe_shape("Project.order", "simple_symbol", false));
assert!(ar_query_safe_shape("Issue.pluck", "simple_symbol", false));
assert!(ar_query_safe_shape("Model.joins", "simple_symbol", false));
}
#[test]
fn parameterised_string_is_safe() {
assert!(ar_query_safe_shape("where", "string", false));
assert!(ar_query_safe_shape("where", "string_literal", false));
}
#[test]
fn interpolated_string_is_dangerous() {
assert!(!ar_query_safe_shape("where", "string", true));
}
#[test]
fn dynamic_identifier_is_dangerous() {
assert!(!ar_query_safe_shape("where", "identifier", false));
}
#[test]
fn array_form_is_safe() {
assert!(ar_query_safe_shape("pluck", "array", false));
}
#[test]
fn non_ar_method_is_never_suppressed() {
assert!(!ar_query_safe_shape("find_by_sql", "string", false));
assert!(!ar_query_safe_shape("connection.execute", "pair", false));
}
#[test]
fn callee_with_module_path_resolves_leaf() {
assert!(ar_query_safe_shape("Foo::Bar.where", "pair", false));
assert!(ar_query_safe_shape("a.b.c.where", "pair", false));
}
}