use crate::flow::{FlowContext, TaintKind, TaintLevel};
use crate::rules::{Rule, create_finding_at_line};
use rma_common::{Confidence, Finding, Language, Severity};
use rma_parser::ParsedFile;
use std::sync::LazyLock;
pub struct DeadStoreRule;
impl Rule for DeadStoreRule {
fn id(&self) -> &str {
"generic/dead-store"
}
fn description(&self) -> &str {
"Variable is assigned but never read before being overwritten or going out of scope"
}
fn applies_to(&self, _lang: Language) -> bool {
true
}
fn check(&self, _parsed: &ParsedFile) -> Vec<Finding> {
Vec::new()
}
fn check_with_flow(&self, parsed: &ParsedFile, flow: &FlowContext) -> Vec<Finding> {
let mut findings = Vec::new();
let dead_stores = flow.dead_stores();
for def in dead_stores {
if should_skip_variable(&def.var_name) {
continue;
}
if super::generic::is_test_or_fixture_file(&parsed.path) {
continue;
}
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
def.line,
&format!("{} = ...", def.var_name),
Severity::Info,
&format!(
"Variable '{}' is assigned on line {} but never read",
def.var_name, def.line
),
parsed.language,
);
finding.confidence = Confidence::Medium;
findings.push(finding);
}
findings
}
fn uses_flow(&self) -> bool {
true
}
}
pub struct UnusedVariableRule;
impl Rule for UnusedVariableRule {
fn id(&self) -> &str {
"generic/unused-variable"
}
fn description(&self) -> &str {
"Variable is declared but never used"
}
fn applies_to(&self, _lang: Language) -> bool {
true
}
fn check(&self, _parsed: &ParsedFile) -> Vec<Finding> {
Vec::new()
}
fn check_with_flow(&self, parsed: &ParsedFile, flow: &FlowContext) -> Vec<Finding> {
let mut findings = Vec::new();
if let Some(chains) = flow.def_use_chains() {
for (def, uses) in &chains.def_to_uses {
if uses.is_empty() && !should_skip_variable(&def.var_name) {
if super::generic::is_test_or_fixture_file(&parsed.path) {
continue;
}
if matches!(
def.origin,
crate::flow::reaching_defs::DefOrigin::Parameter(_)
) {
continue;
}
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
def.line,
&def.var_name,
Severity::Info,
&format!(
"Variable '{}' is declared on line {} but never used",
def.var_name, def.line
),
parsed.language,
);
finding.confidence = Confidence::Medium;
findings.push(finding);
}
}
}
findings
}
fn uses_flow(&self) -> bool {
true
}
}
pub struct CrossFunctionTaintRule;
impl Rule for CrossFunctionTaintRule {
fn id(&self) -> &str {
"generic/cross-function-taint"
}
fn description(&self) -> &str {
"Tainted data flows from one function to a sink in another function"
}
fn applies_to(&self, _lang: Language) -> bool {
true
}
fn check(&self, _parsed: &ParsedFile) -> Vec<Finding> {
Vec::new()
}
fn check_with_flow(&self, parsed: &ParsedFile, flow: &FlowContext) -> Vec<Finding> {
let mut findings = Vec::new();
if let Some(interproc) = flow.interprocedural_result() {
for taint_flow in interproc.interprocedural_flows() {
if super::generic::is_test_or_fixture_file(&parsed.path) {
continue;
}
let functions_str = taint_flow.functions_involved.join(" -> ");
let kind_str = format!("{:?}", taint_flow.source.kind);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
taint_flow.sink.line,
&taint_flow.sink.name,
Severity::Error,
&format!(
"Tainted data ({}) flows from '{}' (line {}) to sink '{}' (line {}) across functions: {}",
kind_str,
taint_flow.source.name,
taint_flow.source.line,
taint_flow.sink.name,
taint_flow.sink.line,
functions_str
),
parsed.language,
);
finding.confidence = Confidence::Medium;
findings.push(finding);
}
}
findings
}
fn uses_flow(&self) -> bool {
true
}
}
pub struct UninitializedVariableRule;
impl Rule for UninitializedVariableRule {
fn id(&self) -> &str {
"generic/uninitialized-variable"
}
fn description(&self) -> &str {
"Variable may be used before being initialized"
}
fn applies_to(&self, lang: Language) -> bool {
matches!(
lang,
Language::JavaScript | Language::TypeScript | Language::Python
)
}
fn check(&self, _parsed: &ParsedFile) -> Vec<Finding> {
Vec::new()
}
fn check_with_flow(&self, parsed: &ParsedFile, flow: &FlowContext) -> Vec<Finding> {
let mut findings = Vec::new();
if let Some(chains) = flow.def_use_chains() {
for (use_site, defs) in &chains.use_to_defs {
if defs.is_empty() && !should_skip_variable(&use_site.var_name) {
if super::generic::is_test_or_fixture_file(&parsed.path) {
continue;
}
if is_likely_global(&use_site.var_name) {
continue;
}
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
use_site.line,
&use_site.var_name,
Severity::Warning,
&format!(
"Variable '{}' may be used on line {} before being initialized",
use_site.var_name, use_site.line
),
parsed.language,
);
finding.confidence = Confidence::Low; findings.push(finding);
}
}
}
findings
}
fn uses_flow(&self) -> bool {
true
}
}
pub struct PathTraversalTaintRule;
impl PathTraversalTaintRule {
const JS_SOURCES: &'static [&'static str] = &[
"req.params",
"req.query",
"req.body",
"request.params",
"request.query",
"request.body",
"req.params.filename",
"req.params.path",
"req.params.file",
"req.query.filename",
"req.query.path",
"req.query.file",
"req.body.filename",
"req.body.path",
"req.body.file",
];
const PYTHON_SOURCES: &'static [&'static str] = &[
"request.args",
"request.form",
"request.files",
"request.values",
"request.GET",
"request.POST",
"request.FILES",
"filename",
"file_path",
"filepath",
];
const GO_SOURCES: &'static [&'static str] = &[
"r.URL.Query",
"r.FormValue",
"r.PostFormValue",
"r.PathValue",
"c.Param",
"c.Query",
"c.PostForm",
"c.QueryParam",
"c.FormValue",
];
const JAVA_SOURCES: &'static [&'static str] = &[
"request.getParameter",
"request.getPathInfo",
"request.getServletPath",
"@PathVariable",
"@RequestParam",
"filename",
"filePath",
"path",
];
const JS_SINKS: &'static [&'static str] = &[
"fs.readFile",
"fs.readFileSync",
"fs.writeFile",
"fs.writeFileSync",
"fs.open",
"fs.openSync",
"fs.access",
"fs.accessSync",
"fs.stat",
"fs.statSync",
"fs.unlink",
"fs.unlinkSync",
"fs.mkdir",
"fs.mkdirSync",
"fs.rmdir",
"fs.rmdirSync",
"fs.readdir",
"fs.readdirSync",
"fs.createReadStream",
"fs.createWriteStream",
"fs.promises.readFile",
"fs.promises.writeFile",
"fs.promises.open",
"path.join",
"path.resolve",
"require",
"import",
];
const PYTHON_SINKS: &'static [&'static str] = &[
"open",
"file",
"os.path.join",
"os.open",
"os.read",
"os.write",
"os.remove",
"os.unlink",
"os.rmdir",
"os.mkdir",
"os.makedirs",
"os.listdir",
"os.stat",
"os.access",
"Path",
"pathlib.Path",
"PurePath",
"shutil.copy",
"shutil.copy2",
"shutil.move",
"shutil.rmtree",
"io.open",
"io.FileIO",
"send_file",
"send_from_directory",
];
const GO_SINKS: &'static [&'static str] = &[
"os.Open",
"os.OpenFile",
"os.Create",
"os.ReadFile",
"os.WriteFile",
"os.Remove",
"os.RemoveAll",
"os.Mkdir",
"os.MkdirAll",
"os.Stat",
"os.Lstat",
"os.ReadDir",
"ioutil.ReadFile",
"ioutil.WriteFile",
"ioutil.ReadDir",
"filepath.Join",
"filepath.Clean",
"http.ServeFile",
"http.FileServer",
];
const JAVA_SINKS: &'static [&'static str] = &[
"new File",
"File",
"FileInputStream",
"FileOutputStream",
"FileReader",
"FileWriter",
"RandomAccessFile",
"Files.readAllBytes",
"Files.readString",
"Files.write",
"Files.writeString",
"Files.copy",
"Files.move",
"Files.delete",
"Files.createFile",
"Files.createDirectory",
"Files.list",
"Files.walk",
"Paths.get",
"Path.of",
"ResourceLoader.getResource",
"ClassPathResource",
];
#[allow(dead_code)]
const JS_SANITIZERS: &'static [&'static str] = &[
"path.basename", "path.normalize", "path.resolve", "sanitize", "sanitizeFilename",
"validatePath",
];
#[allow(dead_code)]
const PYTHON_SANITIZERS: &'static [&'static str] = &[
"os.path.basename", "os.path.realpath", "os.path.abspath", "secure_filename", "sanitize_filename",
"validate_path",
];
#[allow(dead_code)]
const GO_SANITIZERS: &'static [&'static str] = &[
"filepath.Base", "filepath.Clean", "filepath.Abs", "SecureJoin", "sanitizePath",
"validatePath",
];
#[allow(dead_code)]
const JAVA_SANITIZERS: &'static [&'static str] = &[
"getCanonicalPath", "toRealPath", "normalize", "FilenameUtils.getName", "sanitizeFilename",
"validatePath",
];
fn is_path_source(&self, expr: &str, language: Language) -> bool {
let sources = match language {
Language::JavaScript | Language::TypeScript => Self::JS_SOURCES,
Language::Python => Self::PYTHON_SOURCES,
Language::Go => Self::GO_SOURCES,
Language::Java => Self::JAVA_SOURCES,
_ => return false,
};
let expr_lower = expr.to_lowercase();
sources.iter().any(|src| {
let src_lower = src.to_lowercase();
expr_lower.contains(&src_lower) || src_lower.contains(&expr_lower)
})
}
fn is_path_sink(&self, func_name: &str, language: Language) -> bool {
let sinks = match language {
Language::JavaScript | Language::TypeScript => Self::JS_SINKS,
Language::Python => Self::PYTHON_SINKS,
Language::Go => Self::GO_SINKS,
Language::Java => Self::JAVA_SINKS,
_ => return false,
};
let func_lower = func_name.to_lowercase();
sinks.iter().any(|sink| {
let sink_lower = sink.to_lowercase();
func_lower.contains(&sink_lower) || func_lower.ends_with(&sink_lower)
})
}
fn get_suggestion(&self, language: Language) -> &'static str {
match language {
Language::JavaScript | Language::TypeScript => {
"Use path.basename() to extract only the filename, or validate the resolved path starts with your intended base directory using path.resolve() with a startsWith check."
}
Language::Python => {
"Use os.path.basename() to extract only the filename, or use os.path.realpath() and verify the result starts with your intended base directory."
}
Language::Go => {
"Use filepath.Base() to extract only the filename, or use filepath.Clean() combined with strings.HasPrefix() to validate the path stays within bounds."
}
Language::Java => {
"Use getCanonicalPath() and verify the result starts with your intended base directory, or use FilenameUtils.getName() from Apache Commons IO."
}
_ => {
"Validate that file paths cannot escape the intended directory using basename extraction or canonical path validation."
}
}
}
}
impl Rule for PathTraversalTaintRule {
fn id(&self) -> &str {
"security/path-traversal-taint"
}
fn description(&self) -> &str {
"Detects path traversal vulnerabilities where user input flows to file operations"
}
fn applies_to(&self, lang: Language) -> bool {
matches!(
lang,
Language::JavaScript
| Language::TypeScript
| Language::Python
| Language::Go
| Language::Java
)
}
fn check(&self, _parsed: &ParsedFile) -> Vec<Finding> {
Vec::new()
}
fn check_with_flow(&self, parsed: &ParsedFile, flow: &FlowContext) -> Vec<Finding> {
let mut findings = Vec::new();
if super::generic::is_test_or_fixture_file(&parsed.path) {
return Vec::new();
}
if let Some(interproc) = flow.interprocedural_result() {
for taint_flow in interproc.flows_by_kind(crate::flow::TaintKind::FilePath) {
if self.is_path_sink(&taint_flow.sink.name, parsed.language) {
let message = format!(
"Path traversal vulnerability: user input '{}' (line {}) flows to file operation '{}' (line {}). {}",
taint_flow.source.name,
taint_flow.source.line,
taint_flow.sink.name,
taint_flow.sink.line,
self.get_suggestion(parsed.language)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
taint_flow.sink.line,
&taint_flow.sink.name,
Severity::Error,
&message,
parsed.language,
);
finding.confidence = Confidence::High;
finding.suggestion = Some(self.get_suggestion(parsed.language).to_string());
findings.push(finding);
}
}
for taint_flow in interproc.flows_by_kind(crate::flow::TaintKind::UserInput) {
if self.is_path_sink(&taint_flow.sink.name, parsed.language) {
let message = format!(
"Potential path traversal: user input '{}' (line {}) may flow to file operation '{}' (line {}). {}",
taint_flow.source.name,
taint_flow.source.line,
taint_flow.sink.name,
taint_flow.sink.line,
self.get_suggestion(parsed.language)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
taint_flow.sink.line,
&taint_flow.sink.name,
Severity::Warning,
&message,
parsed.language,
);
finding.confidence = Confidence::Medium;
finding.suggestion = Some(self.get_suggestion(parsed.language).to_string());
findings.push(finding);
}
}
}
for (var_name, _info) in flow.symbols.iter() {
if !flow.is_tainted(var_name) {
continue;
}
let var_lower = var_name.to_lowercase();
let is_path_var = var_lower.contains("path")
|| var_lower.contains("file")
|| var_lower.contains("filename")
|| var_lower.contains("dir")
|| var_lower.contains("folder");
if is_path_var && self.is_path_source(var_name, parsed.language) {
if let Some(interproc) = flow.interprocedural_result() {
for call_site in &interproc.call_sites {
if self.is_path_sink(&call_site.callee_name, parsed.language) {
for arg in &call_site.arguments {
if arg.var_name.as_ref().is_some_and(|n| n == var_name)
|| arg.expr.contains(var_name)
{
let message = format!(
"Path traversal risk: tainted variable '{}' used in file operation '{}' on line {}. {}",
var_name,
call_site.callee_name,
call_site.line,
self.get_suggestion(parsed.language)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
call_site.line,
&call_site.callee_name,
Severity::Warning,
&message,
parsed.language,
);
finding.confidence = Confidence::Medium;
finding.suggestion =
Some(self.get_suggestion(parsed.language).to_string());
findings.push(finding);
}
}
}
}
}
}
}
findings.sort_by_key(|f| (f.location.start_line, f.location.start_column));
findings.dedup_by(|a, b| {
a.location.start_line == b.location.start_line
&& a.location.start_column == b.location.start_column
});
findings
}
fn uses_flow(&self) -> bool {
true
}
}
pub struct CommandInjectionTaintRule;
impl CommandInjectionTaintRule {
const JS_SOURCES: &'static [&'static str] = &[
"req.query",
"req.body",
"req.params",
"request.query",
"request.body",
"request.params",
"process.argv",
"process.env",
"process.stdin",
"url.searchParams",
];
const PYTHON_SOURCES: &'static [&'static str] = &[
"request.args",
"request.form",
"request.values",
"request.json",
"request.GET",
"request.POST",
"sys.argv",
"os.environ",
"os.getenv",
"input",
"sys.stdin",
];
const GO_SOURCES: &'static [&'static str] = &[
"r.URL.Query",
"r.FormValue",
"r.PostFormValue",
"r.PathValue",
"os.Args",
"os.Getenv",
"os.LookupEnv",
"c.Param",
"c.Query",
"c.PostForm",
"c.QueryParam",
"c.FormValue",
"bufio.Scanner",
];
const RUST_SOURCES: &'static [&'static str] = &[
"std::env::args",
"env::args",
"args",
"std::env::var",
"env::var",
"var",
"env::var_os",
"std::io::stdin",
"io::stdin",
"Query",
"Form",
"Path",
"Json",
];
const JAVA_SOURCES: &'static [&'static str] = &[
"request.getParameter",
"request.getParameterValues",
"request.getQueryString",
"request.getInputStream",
"System.getenv",
"System.getProperty",
"args",
"System.in",
"Scanner",
"@RequestParam",
"@PathVariable",
"@RequestBody",
];
const JS_SINKS: &'static [&'static str] = &[
"child_process.exec",
"child_process.execSync",
"child_process.spawn",
"child_process.spawnSync",
"child_process.execFile",
"child_process.execFileSync",
"child_process.fork",
"shell.exec",
"execa",
"execaSync",
"shelljs.exec",
];
const PYTHON_SINKS: &'static [&'static str] = &[
"subprocess.call",
"subprocess.run",
"subprocess.Popen",
"subprocess.check_call",
"subprocess.check_output",
"subprocess.getstatusoutput",
"subprocess.getoutput",
"os.system",
"os.popen",
"os.popen2",
"os.popen3",
"os.popen4",
"os.execl",
"os.execle",
"os.execlp",
"os.execlpe",
"os.execv",
"os.execve",
"os.execvp",
"os.execvpe",
"os.spawnl",
"os.spawnle",
"os.spawnlp",
"os.spawnlpe",
"os.spawnv",
"os.spawnve",
"os.spawnvp",
"os.spawnvpe",
"commands.getoutput",
"commands.getstatusoutput",
];
const GO_SINKS: &'static [&'static str] = &[
"exec.Command",
"exec.CommandContext",
"os.StartProcess",
"syscall.Exec",
"syscall.ForkExec",
];
const RUST_SINKS: &'static [&'static str] = &[
"Command::new",
"std::process::Command::new",
"process::Command::new",
"tokio::process::Command::new",
"async_std::process::Command::new",
];
const JAVA_SINKS: &'static [&'static str] = &[
"Runtime.getRuntime",
"Runtime.exec",
"runtime.exec",
"ProcessBuilder",
"new ProcessBuilder",
"CommandLine",
"DefaultExecutor",
"Executor.execute",
];
const SHELL_MODE_PATTERNS: &'static [&'static str] = &[
"shell=True",
"shell = True",
"shell: true",
"shell:true",
"sh -c",
"bash -c",
"cmd /c",
"cmd.exe /c",
"powershell -c",
"pwsh -c",
"/bin/sh",
"/bin/bash",
];
fn is_command_source(&self, expr: &str, language: Language) -> bool {
let sources = match language {
Language::JavaScript | Language::TypeScript => Self::JS_SOURCES,
Language::Python => Self::PYTHON_SOURCES,
Language::Go => Self::GO_SOURCES,
Language::Rust => Self::RUST_SOURCES,
Language::Java => Self::JAVA_SOURCES,
_ => return false,
};
let expr_lower = expr.to_lowercase();
sources.iter().any(|src| {
let src_lower = src.to_lowercase();
expr_lower.contains(&src_lower) || src_lower.contains(&expr_lower)
})
}
fn is_command_sink(&self, func_name: &str, language: Language) -> bool {
let sinks = match language {
Language::JavaScript | Language::TypeScript => Self::JS_SINKS,
Language::Python => Self::PYTHON_SINKS,
Language::Go => Self::GO_SINKS,
Language::Rust => Self::RUST_SINKS,
Language::Java => Self::JAVA_SINKS,
_ => return false,
};
let func_lower = func_name.to_lowercase();
sinks.iter().any(|sink| {
let sink_lower = sink.to_lowercase();
func_lower.contains(&sink_lower) || func_lower.ends_with(&sink_lower)
})
}
fn has_shell_mode(&self, code_context: &str) -> bool {
let context_lower = code_context.to_lowercase();
Self::SHELL_MODE_PATTERNS
.iter()
.any(|pattern| context_lower.contains(&pattern.to_lowercase()))
}
fn get_suggestion(&self, language: Language, is_shell_mode: bool) -> String {
match language {
Language::JavaScript | Language::TypeScript => {
if is_shell_mode {
"CRITICAL: Avoid shell mode. Use execFile() or spawn() with array arguments. If shell mode is required, use shell-escape."
} else {
"Pass command arguments as an array to spawn() or execFile(). Never construct command strings from user input."
}
}
Language::Python => {
if is_shell_mode {
"CRITICAL: Avoid shell=True with subprocess. Pass command as a list. If shell mode is required, use shlex.quote()."
} else {
"Pass command as a list to subprocess functions instead of a string. Use shlex.quote() if you must include user input."
}
}
Language::Go => {
"Pass command arguments as separate strings to exec.Command() instead of constructing a shell command. Never use 'sh -c' with user input."
}
Language::Rust => {
"Pass arguments to Command::new().arg() separately instead of concatenating. Use shell-escape crate if shell expansion is needed."
}
Language::Java => {
if is_shell_mode {
"CRITICAL: Avoid passing command strings to Runtime. Use ProcessBuilder with separate arguments."
} else {
"Use ProcessBuilder with command and arguments as separate strings. Never concatenate user input into command strings."
}
}
_ => "Avoid constructing shell commands from user input. Use parameterized APIs or proper escaping.",
}.to_string()
}
fn determine_severity(&self, is_shell_mode: bool) -> Severity {
if is_shell_mode {
Severity::Error
} else {
Severity::Warning
}
}
}
impl Rule for CommandInjectionTaintRule {
fn id(&self) -> &str {
"security/command-injection-taint"
}
fn description(&self) -> &str {
"Detects command injection vulnerabilities where user input flows to command execution"
}
fn applies_to(&self, lang: Language) -> bool {
matches!(
lang,
Language::JavaScript
| Language::TypeScript
| Language::Python
| Language::Go
| Language::Rust
| Language::Java
)
}
fn check(&self, _parsed: &ParsedFile) -> Vec<Finding> {
Vec::new()
}
fn check_with_flow(&self, parsed: &ParsedFile, flow: &FlowContext) -> Vec<Finding> {
let mut findings = Vec::new();
if super::generic::is_test_or_fixture_file(&parsed.path) {
return Vec::new();
}
if let Some(interproc) = flow.interprocedural_result() {
for taint_flow in interproc.flows_by_kind(TaintKind::Command) {
if self.is_command_sink(&taint_flow.sink.name, parsed.language) {
let is_shell_mode = self.has_shell_mode(&taint_flow.sink.name);
let severity = self.determine_severity(is_shell_mode);
let risk_level = if is_shell_mode { "CRITICAL" } else { "High" };
let message = format!(
"Command injection vulnerability ({}): user input '{}' (line {}) flows to command execution '{}' (line {}). {}",
risk_level,
taint_flow.source.name,
taint_flow.source.line,
taint_flow.sink.name,
taint_flow.sink.line,
self.get_suggestion(parsed.language, is_shell_mode)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
taint_flow.sink.line,
&taint_flow.sink.name,
severity,
&message,
parsed.language,
);
finding.confidence = Confidence::High;
finding.suggestion = Some(self.get_suggestion(parsed.language, is_shell_mode));
findings.push(finding);
}
}
for taint_flow in interproc.flows_by_kind(TaintKind::UserInput) {
if self.is_command_sink(&taint_flow.sink.name, parsed.language) {
let is_shell_mode = self.has_shell_mode(&taint_flow.sink.name);
let severity = self.determine_severity(is_shell_mode);
let message = format!(
"Potential command injection: user input '{}' (line {}) may flow to command execution '{}' (line {}). {}",
taint_flow.source.name,
taint_flow.source.line,
taint_flow.sink.name,
taint_flow.sink.line,
self.get_suggestion(parsed.language, is_shell_mode)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
taint_flow.sink.line,
&taint_flow.sink.name,
severity,
&message,
parsed.language,
);
finding.confidence = Confidence::Medium;
finding.suggestion = Some(self.get_suggestion(parsed.language, is_shell_mode));
findings.push(finding);
}
}
}
for (var_name, _info) in flow.symbols.iter() {
if !flow.is_tainted(var_name) {
continue;
}
let var_lower = var_name.to_lowercase();
let is_cmd_var = var_lower.contains("cmd")
|| var_lower.contains("command")
|| var_lower.contains("shell")
|| var_lower.contains("script");
if is_cmd_var || self.is_command_source(var_name, parsed.language) {
if let Some(interproc) = flow.interprocedural_result() {
for call_site in &interproc.call_sites {
if self.is_command_sink(&call_site.callee_name, parsed.language) {
for arg in &call_site.arguments {
if arg.var_name.as_ref().is_some_and(|n| n == var_name)
|| arg.expr.contains(var_name)
{
let is_shell_mode = self.has_shell_mode(&call_site.callee_name);
let severity = self.determine_severity(is_shell_mode);
let message = format!(
"Command injection risk: tainted variable '{}' used in command execution '{}' on line {}. {}",
var_name,
call_site.callee_name,
call_site.line,
self.get_suggestion(parsed.language, is_shell_mode)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
call_site.line,
&call_site.callee_name,
severity,
&message,
parsed.language,
);
finding.confidence = Confidence::Medium;
finding.suggestion =
Some(self.get_suggestion(parsed.language, is_shell_mode));
findings.push(finding);
}
}
}
}
}
}
}
findings.sort_by_key(|f| (f.location.start_line, f.location.start_column));
findings.dedup_by(|a, b| {
a.location.start_line == b.location.start_line
&& a.location.start_column == b.location.start_column
});
findings
}
fn uses_flow(&self) -> bool {
true
}
}
pub struct SqlInjectionTaintRule;
impl SqlInjectionTaintRule {
const JS_SQL_SINKS: &'static [&'static str] = &[
"query",
"execute",
"exec",
"run",
"mysql.query",
"mysql.execute",
"connection.query",
"connection.execute",
"pool.query",
"pool.execute",
"pg.query",
"client.query",
"pool.query",
"$queryRaw",
"$executeRaw",
"$queryRawUnsafe",
"$executeRawUnsafe",
"knex.raw",
"raw",
"sequelize.query",
"db.prepare",
"db.exec",
"createQueryBuilder",
"manager.query",
"collection.find",
"collection.findOne",
"collection.aggregate",
"db.collection",
];
const PYTHON_SQL_SINKS: &'static [&'static str] = &[
"cursor.execute",
"cursor.executemany",
"cursor.executescript",
"connection.execute",
"conn.execute",
"db.execute",
"session.execute",
"engine.execute",
"text",
"raw_connection",
"raw",
"extra",
"RawSQL",
"cursor.execute",
"cur.execute",
"cursor.execute",
"execute",
"executemany",
"executescript",
"connection.fetch",
"connection.execute",
"collection.find",
"collection.find_one",
"collection.aggregate",
];
const GO_SQL_SINKS: &'static [&'static str] = &[
"db.Query",
"db.QueryRow",
"db.QueryContext",
"db.QueryRowContext",
"db.Exec",
"db.ExecContext",
"db.Prepare",
"db.PrepareContext",
"tx.Query",
"tx.QueryRow",
"tx.Exec",
"stmt.Query",
"stmt.QueryRow",
"stmt.Exec",
"db.Raw",
"db.Exec",
"db.Where",
"tx.Raw",
"sqlx.Query",
"sqlx.QueryRow",
"sqlx.Exec",
"sqlx.Get",
"sqlx.Select",
"collection.Find",
"collection.FindOne",
"collection.Aggregate",
];
const JAVA_SQL_SINKS: &'static [&'static str] = &[
"Statement.execute",
"Statement.executeQuery",
"Statement.executeUpdate",
"Statement.executeBatch",
"PreparedStatement.execute",
"PreparedStatement.executeQuery",
"PreparedStatement.executeUpdate",
"connection.createStatement",
"connection.prepareStatement",
"session.createQuery",
"session.createSQLQuery",
"session.createNativeQuery",
"entityManager.createQuery",
"entityManager.createNativeQuery",
"jdbcTemplate.query",
"jdbcTemplate.queryForObject",
"jdbcTemplate.queryForList",
"jdbcTemplate.execute",
"jdbcTemplate.update",
"namedParameterJdbcTemplate.query",
"sqlSession.selectOne",
"sqlSession.selectList",
"sqlSession.insert",
"sqlSession.update",
"sqlSession.delete",
];
#[allow(dead_code)]
const JS_SOURCES: &'static [&'static str] = &[
"req.params",
"req.query",
"req.body",
"request.params",
"request.query",
"request.body",
"ctx.params",
"ctx.query",
"ctx.request.body",
];
#[allow(dead_code)]
const PYTHON_SOURCES: &'static [&'static str] = &[
"request.args",
"request.form",
"request.json",
"request.data",
"request.GET",
"request.POST",
];
#[allow(dead_code)]
const GO_SOURCES: &'static [&'static str] = &[
"r.URL.Query",
"r.FormValue",
"r.PostFormValue",
"c.Param",
"c.Query",
"c.PostForm",
];
#[allow(dead_code)]
const JAVA_SOURCES: &'static [&'static str] = &[
"request.getParameter",
"@RequestParam",
"@PathVariable",
"@RequestBody",
];
fn is_sql_sink(&self, func_name: &str, language: Language) -> bool {
let sinks = match language {
Language::JavaScript | Language::TypeScript => Self::JS_SQL_SINKS,
Language::Python => Self::PYTHON_SQL_SINKS,
Language::Go => Self::GO_SQL_SINKS,
Language::Java => Self::JAVA_SQL_SINKS,
_ => return false,
};
let func_lower = func_name.to_lowercase();
sinks.iter().any(|sink| {
let sink_lower = sink.to_lowercase();
func_lower.contains(&sink_lower) || func_lower.ends_with(&sink_lower)
})
}
fn is_parameterized_query(query: &str) -> bool {
if query.contains('?') {
return true;
}
if query.contains("$1") || query.contains("$2") || query.contains("$3") {
return true;
}
let has_named_param = regex::Regex::new(r":\w+").map_or(false, |re| re.is_match(query));
if has_named_param {
return true;
}
if query.contains('@') && regex::Regex::new(r"@\w+").map_or(false, |re| re.is_match(query))
{
return true;
}
if query.contains("%s") || query.contains("%(") {
return true;
}
false
}
fn get_suggestion(&self, language: Language) -> &'static str {
match language {
Language::JavaScript | Language::TypeScript => {
"Use parameterized queries with placeholders (?) instead of string concatenation. Example: db.query('SELECT * FROM users WHERE id = ?', [userId])"
}
Language::Python => {
"Use parameterized queries with placeholders (%s or ?) instead of string formatting. Example: cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))"
}
Language::Go => {
"Use parameterized queries with placeholders ($1, $2, or ?) instead of fmt.Sprintf. Example: db.Query('SELECT * FROM users WHERE id = $1', userId)"
}
Language::Java => {
"Use PreparedStatement with placeholders (?) instead of Statement with string concatenation. Example: PreparedStatement ps = conn.prepareStatement('SELECT * FROM users WHERE id = ?'); ps.setInt(1, userId);"
}
_ => {
"Use parameterized queries with placeholders instead of string concatenation to prevent SQL injection."
}
}
}
}
impl Rule for SqlInjectionTaintRule {
fn id(&self) -> &str {
"security/sql-injection-taint"
}
fn description(&self) -> &str {
"Detects SQL injection vulnerabilities where user input flows to SQL execution"
}
fn applies_to(&self, lang: Language) -> bool {
matches!(
lang,
Language::JavaScript
| Language::TypeScript
| Language::Python
| Language::Go
| Language::Java
)
}
fn check(&self, _parsed: &ParsedFile) -> Vec<Finding> {
Vec::new()
}
fn check_with_flow(&self, parsed: &ParsedFile, flow: &FlowContext) -> Vec<Finding> {
let mut findings = Vec::new();
if super::generic::is_test_or_fixture_file(&parsed.path) {
return Vec::new();
}
if let Some(interproc) = flow.interprocedural_result() {
for taint_flow in interproc.flows_by_kind(crate::flow::TaintKind::SqlQuery) {
if self.is_sql_sink(&taint_flow.sink.name, parsed.language) {
let message = format!(
"SQL injection vulnerability: user input '{}' (line {}) flows to SQL operation '{}' (line {}). {}",
taint_flow.source.name,
taint_flow.source.line,
taint_flow.sink.name,
taint_flow.sink.line,
self.get_suggestion(parsed.language)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
taint_flow.sink.line,
&taint_flow.sink.name,
Severity::Error,
&message,
parsed.language,
);
finding.confidence = Confidence::High;
finding.suggestion = Some(self.get_suggestion(parsed.language).to_string());
findings.push(finding);
}
}
for taint_flow in interproc.flows_by_kind(crate::flow::TaintKind::UserInput) {
if self.is_sql_sink(&taint_flow.sink.name, parsed.language) {
let message = format!(
"Potential SQL injection: user input '{}' (line {}) may flow to SQL operation '{}' (line {}). {}",
taint_flow.source.name,
taint_flow.source.line,
taint_flow.sink.name,
taint_flow.sink.line,
self.get_suggestion(parsed.language)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
taint_flow.sink.line,
&taint_flow.sink.name,
Severity::Warning,
&message,
parsed.language,
);
finding.confidence = Confidence::Medium;
finding.suggestion = Some(self.get_suggestion(parsed.language).to_string());
findings.push(finding);
}
}
}
for (var_name, _info) in flow.symbols.iter() {
if !flow.is_tainted(var_name) {
continue;
}
let var_lower = var_name.to_lowercase();
let is_sql_related = var_lower.contains("query")
|| var_lower.contains("sql")
|| var_lower.contains("stmt")
|| var_lower.contains("statement");
let is_user_input = var_lower.contains("input")
|| var_lower.contains("param")
|| var_lower.contains("user");
if is_sql_related || is_user_input {
if let Some(interproc) = flow.interprocedural_result() {
for call_site in &interproc.call_sites {
if self.is_sql_sink(&call_site.callee_name, parsed.language) {
for arg in &call_site.arguments {
let uses_tainted =
arg.var_name.as_ref().is_some_and(|n| n == var_name)
|| arg.expr.contains(var_name);
if uses_tainted && !Self::is_parameterized_query(&arg.expr) {
let has_concat = arg.expr.contains('+')
|| arg.expr.contains("format")
|| arg.expr.contains("sprintf")
|| arg.expr.contains('$')
|| arg.expr.contains('{');
let (severity, confidence) = if has_concat {
(Severity::Error, Confidence::High)
} else {
(Severity::Warning, Confidence::Medium)
};
let message = format!(
"SQL injection risk: tainted variable '{}' used in SQL operation '{}' on line {}{}. {}",
var_name,
call_site.callee_name,
call_site.line,
if has_concat {
" with string concatenation"
} else {
""
},
self.get_suggestion(parsed.language)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
call_site.line,
&call_site.callee_name,
severity,
&message,
parsed.language,
);
finding.confidence = confidence;
finding.suggestion =
Some(self.get_suggestion(parsed.language).to_string());
findings.push(finding);
}
}
}
}
}
}
}
findings.sort_by_key(|f| (f.location.start_line, f.location.start_column));
findings.dedup_by(|a, b| {
a.location.start_line == b.location.start_line
&& a.location.start_column == b.location.start_column
});
findings
}
fn uses_flow(&self) -> bool {
true
}
}
pub struct SsrfTaintRule;
static PRIVATE_IP_PATTERNS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
vec![
"127.", "10.", "172.16.",
"172.17.",
"172.18.",
"172.19.",
"172.20.",
"172.21.",
"172.22.",
"172.23.",
"172.24.",
"172.25.",
"172.26.",
"172.27.",
"172.28.",
"172.29.",
"172.30.",
"172.31.", "192.168.", "169.254.", "0.0.0.0", "localhost", "[::1]", "[::ffff:127", "metadata", "169.254.169.254", ]
});
impl SsrfTaintRule {
const JS_SOURCES: &'static [&'static str] = &[
"req.params",
"req.query",
"req.body",
"request.params",
"request.query",
"request.body",
"req.params.url",
"req.query.url",
"req.body.url",
"req.params.target",
"req.query.target",
"req.body.target",
"req.params.redirect",
"req.query.redirect",
"req.body.redirect",
"req.params.callback",
"req.query.callback",
"req.body.callback",
"req.params.endpoint",
"req.query.endpoint",
"req.body.endpoint",
"req.params.uri",
"req.query.uri",
"req.body.uri",
"req.params.host",
"req.query.host",
"req.body.host",
"req.params.link",
"req.query.link",
"req.body.link",
];
const PYTHON_SOURCES: &'static [&'static str] = &[
"request.args.get('url')",
"request.args.get('target')",
"request.args.get('redirect')",
"request.args.get('callback')",
"request.args.get('endpoint')",
"request.args.get('uri')",
"request.args.get('host')",
"request.args.get('link')",
"request.form.get('url')",
"request.form.get('target')",
"request.json.get('url')",
"request.json.get('target')",
"request.args",
"request.form",
"request.json",
"request.GET.get('url')",
"request.POST.get('url')",
"request.GET.get('target')",
"request.POST.get('target')",
"request.GET",
"request.POST",
];
const GO_SOURCES: &'static [&'static str] = &[
"r.URL.Query().Get(\"url\")",
"r.URL.Query().Get(\"target\")",
"r.URL.Query().Get(\"redirect\")",
"r.URL.Query().Get(\"callback\")",
"r.FormValue(\"url\")",
"r.FormValue(\"target\")",
"r.PostFormValue(\"url\")",
"r.PostFormValue(\"target\")",
"r.URL.Query",
"r.FormValue",
"r.PostFormValue",
"c.Query(\"url\")",
"c.Query(\"target\")",
"c.Param(\"url\")",
"c.Param(\"target\")",
"c.PostForm(\"url\")",
"c.PostForm(\"target\")",
"c.Query",
"c.Param",
"c.PostForm",
"c.QueryParam(\"url\")",
"c.QueryParam(\"target\")",
"c.FormValue(\"url\")",
"c.FormValue(\"target\")",
];
const JAVA_SOURCES: &'static [&'static str] = &[
"request.getParameter(\"url\")",
"request.getParameter(\"target\")",
"request.getParameter(\"redirect\")",
"request.getParameter(\"callback\")",
"request.getParameter(\"endpoint\")",
"request.getParameter(\"uri\")",
"request.getParameter(\"host\")",
"request.getParameter(\"link\")",
"request.getParameter",
"@RequestParam(\"url\")",
"@RequestParam(\"target\")",
"@PathVariable(\"url\")",
"@PathVariable(\"target\")",
"@RequestParam",
"@PathVariable",
];
const JS_SINKS: &'static [&'static str] = &[
"fetch",
"globalThis.fetch",
"http.get",
"http.request",
"https.get",
"https.request",
"axios",
"axios.get",
"axios.post",
"axios.put",
"axios.delete",
"axios.patch",
"axios.head",
"axios.options",
"axios.request",
"node-fetch",
"got",
"got.get",
"got.post",
"got.put",
"got.delete",
"request",
"request.get",
"request.post",
"superagent",
"superagent.get",
"superagent.post",
"needle",
"needle.get",
"needle.post",
];
const PYTHON_SINKS: &'static [&'static str] = &[
"requests.get",
"requests.post",
"requests.put",
"requests.delete",
"requests.patch",
"requests.head",
"requests.options",
"requests.request",
"urllib.request.urlopen",
"urllib.request.Request",
"urllib2.urlopen",
"urllib2.Request",
"urlopen",
"http.client.HTTPConnection",
"http.client.HTTPSConnection",
"HTTPConnection",
"HTTPSConnection",
"httpx.get",
"httpx.post",
"httpx.put",
"httpx.delete",
"httpx.AsyncClient",
"httpx.Client",
"aiohttp.ClientSession",
"session.get",
"session.post",
"httplib2.Http",
"pycurl.Curl",
];
const GO_SINKS: &'static [&'static str] = &[
"http.Get",
"http.Post",
"http.PostForm",
"http.Head",
"http.NewRequest",
"http.NewRequestWithContext",
"client.Get",
"client.Post",
"client.Do",
"transport.RoundTrip",
"resty.R",
"req.Get",
"req.Post",
"fasthttp.Get",
"fasthttp.Post",
];
const JAVA_SINKS: &'static [&'static str] = &[
"URL.openConnection",
"URL.openStream",
"url.openConnection",
"url.openStream",
"new URL",
"HttpURLConnection",
"HttpsURLConnection",
"HttpClient.execute",
"HttpClients.createDefault",
"CloseableHttpClient",
"HttpGet",
"HttpPost",
"HttpPut",
"HttpDelete",
"OkHttpClient",
"okHttpClient.newCall",
"Request.Builder",
"RestTemplate",
"restTemplate.getForObject",
"restTemplate.getForEntity",
"restTemplate.postForObject",
"restTemplate.postForEntity",
"restTemplate.exchange",
"WebClient",
"webClient.get",
"webClient.post",
"Client",
"client.target",
];
fn is_ssrf_source(&self, expr: &str, language: Language) -> bool {
let sources = match language {
Language::JavaScript | Language::TypeScript => Self::JS_SOURCES,
Language::Python => Self::PYTHON_SOURCES,
Language::Go => Self::GO_SOURCES,
Language::Java => Self::JAVA_SOURCES,
_ => return false,
};
let expr_lower = expr.to_lowercase();
sources.iter().any(|src| {
let src_lower = src.to_lowercase();
expr_lower.contains(&src_lower) || src_lower.contains(&expr_lower)
})
}
fn is_http_sink(&self, func_name: &str, language: Language) -> bool {
let sinks = match language {
Language::JavaScript | Language::TypeScript => Self::JS_SINKS,
Language::Python => Self::PYTHON_SINKS,
Language::Go => Self::GO_SINKS,
Language::Java => Self::JAVA_SINKS,
_ => return false,
};
let func_lower = func_name.to_lowercase();
sinks.iter().any(|sink| {
let sink_lower = sink.to_lowercase();
func_lower.contains(&sink_lower) || func_lower.ends_with(&sink_lower)
})
}
fn is_safe_url_literal(url: &str) -> bool {
let url_lower = url.to_lowercase();
if !url_lower.starts_with("http://") && !url_lower.starts_with("https://") {
return false;
}
for pattern in PRIVATE_IP_PATTERNS.iter() {
if url_lower.contains(pattern) {
return false;
}
}
true
}
fn is_url_variable(&self, var_name: &str) -> bool {
let var_lower = var_name.to_lowercase();
var_lower.contains("url")
|| var_lower.contains("uri")
|| var_lower.contains("target")
|| var_lower.contains("redirect")
|| var_lower.contains("callback")
|| var_lower.contains("endpoint")
|| var_lower.contains("host")
|| var_lower.contains("link")
|| var_lower.contains("href")
|| var_lower.contains("src")
|| var_lower.contains("dest")
|| var_lower.contains("destination")
}
fn get_suggestion(&self, language: Language) -> &'static str {
match language {
Language::JavaScript | Language::TypeScript => {
"Validate URLs against an allowlist of trusted domains. Block private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16). Only allow http/https schemes."
}
Language::Python => {
"Use the ipaddress module to validate IPs are not private. Validate URLs against an allowlist. Block schemes other than http/https. Use urllib.parse.urlparse() to validate hostnames."
}
Language::Go => {
"Use net.ParseIP() to check if resolved IP is not in private ranges. Validate URL scheme is http/https. Use url.Parse() to extract and validate hostname against an allowlist."
}
Language::Java => {
"Use InetAddress methods (isLoopbackAddress, isSiteLocalAddress, isLinkLocalAddress) to block private IPs. Validate URLs against an allowlist. Use URI.getHost() to validate hostnames."
}
_ => {
"Validate URLs against an allowlist. Block private IP ranges and non-http(s) schemes."
}
}
}
}
impl Rule for SsrfTaintRule {
fn id(&self) -> &str {
"security/ssrf-taint"
}
fn description(&self) -> &str {
"Detects SSRF vulnerabilities where user-controlled URLs flow to HTTP clients"
}
fn applies_to(&self, lang: Language) -> bool {
matches!(
lang,
Language::JavaScript
| Language::TypeScript
| Language::Python
| Language::Go
| Language::Java
)
}
fn check(&self, _parsed: &ParsedFile) -> Vec<Finding> {
Vec::new()
}
fn check_with_flow(&self, parsed: &ParsedFile, flow: &FlowContext) -> Vec<Finding> {
let mut findings = Vec::new();
if super::generic::is_test_or_fixture_file(&parsed.path) {
return Vec::new();
}
if let Some(interproc) = flow.interprocedural_result() {
for taint_flow in interproc.flows_by_kind(TaintKind::Url) {
if self.is_http_sink(&taint_flow.sink.name, parsed.language) {
let message = format!(
"SSRF vulnerability: user-controlled URL '{}' (line {}) flows to HTTP client '{}' (line {}). {}",
taint_flow.source.name,
taint_flow.source.line,
taint_flow.sink.name,
taint_flow.sink.line,
self.get_suggestion(parsed.language)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
taint_flow.sink.line,
&taint_flow.sink.name,
Severity::Error,
&message,
parsed.language,
);
finding.confidence = Confidence::High;
finding.suggestion = Some(self.get_suggestion(parsed.language).to_string());
findings.push(finding);
}
}
for taint_flow in interproc.flows_by_kind(TaintKind::UserInput) {
if self.is_http_sink(&taint_flow.sink.name, parsed.language) {
let message = format!(
"Potential SSRF: user input '{}' (line {}) may flow to HTTP client '{}' (line {}). {}",
taint_flow.source.name,
taint_flow.source.line,
taint_flow.sink.name,
taint_flow.sink.line,
self.get_suggestion(parsed.language)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
taint_flow.sink.line,
&taint_flow.sink.name,
Severity::Warning,
&message,
parsed.language,
);
finding.confidence = Confidence::Medium;
finding.suggestion = Some(self.get_suggestion(parsed.language).to_string());
findings.push(finding);
}
}
}
for (var_name, _info) in flow.symbols.iter() {
if !flow.is_tainted(var_name) {
continue;
}
let is_url_var = self.is_url_variable(var_name);
if is_url_var && self.is_ssrf_source(var_name, parsed.language) {
if let Some(interproc) = flow.interprocedural_result() {
for call_site in &interproc.call_sites {
if self.is_http_sink(&call_site.callee_name, parsed.language) {
for arg in &call_site.arguments {
let uses_tainted =
arg.var_name.as_ref().is_some_and(|n| n == var_name)
|| arg.expr.contains(var_name);
if uses_tainted && !Self::is_safe_url_literal(&arg.expr) {
let message = format!(
"SSRF risk: tainted URL variable '{}' used in HTTP client '{}' on line {}. {}",
var_name,
call_site.callee_name,
call_site.line,
self.get_suggestion(parsed.language)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
call_site.line,
&call_site.callee_name,
Severity::Warning,
&message,
parsed.language,
);
finding.confidence = Confidence::Medium;
finding.suggestion =
Some(self.get_suggestion(parsed.language).to_string());
findings.push(finding);
}
}
}
}
}
}
}
if let Some(interproc) = flow.interprocedural_result() {
for call_site in &interproc.call_sites {
if self.is_http_sink(&call_site.callee_name, parsed.language) {
if let Some(first_arg) = call_site.arguments.first() {
if let Some(ref var_name) = first_arg.var_name {
let taint_level = flow.taint_level_at(var_name, call_site.node_id);
match taint_level {
TaintLevel::Full => {
let message = format!(
"SSRF vulnerability: variable '{}' used as URL in '{}' is tainted. {}",
var_name,
call_site.callee_name,
self.get_suggestion(parsed.language)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
call_site.line,
&call_site.callee_name,
Severity::Error,
&message,
parsed.language,
);
finding.confidence = Confidence::High;
finding.suggestion =
Some(self.get_suggestion(parsed.language).to_string());
findings.push(finding);
}
TaintLevel::Partial => {
let message = format!(
"Potential SSRF: variable '{}' used as URL in '{}' may be tainted on some paths. {}",
var_name,
call_site.callee_name,
self.get_suggestion(parsed.language)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
call_site.line,
&call_site.callee_name,
Severity::Warning,
&message,
parsed.language,
);
finding.confidence = Confidence::Medium;
finding.suggestion =
Some(self.get_suggestion(parsed.language).to_string());
findings.push(finding);
}
TaintLevel::Clean => {
if self.is_url_variable(var_name) {
let message = format!(
"SSRF review: URL variable '{}' used in HTTP client '{}'. Verify URL cannot be user-controlled. {}",
var_name,
call_site.callee_name,
self.get_suggestion(parsed.language)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
call_site.line,
&call_site.callee_name,
Severity::Info,
&message,
parsed.language,
);
finding.confidence = Confidence::Low;
finding.suggestion =
Some(self.get_suggestion(parsed.language).to_string());
findings.push(finding);
}
}
}
} else if !first_arg.expr.starts_with('"')
&& !first_arg.expr.starts_with('\'')
{
for pattern in PRIVATE_IP_PATTERNS.iter() {
if first_arg.expr.contains(pattern) {
let message = format!(
"Suspicious SSRF: HTTP request contains internal address pattern '{}' in '{}'. {}",
pattern,
call_site.callee_name,
self.get_suggestion(parsed.language)
);
let mut finding = create_finding_at_line(
self.id(),
&parsed.path,
call_site.line,
&call_site.callee_name,
Severity::Warning,
&message,
parsed.language,
);
finding.confidence = Confidence::Medium;
finding.suggestion =
Some(self.get_suggestion(parsed.language).to_string());
findings.push(finding);
break;
}
}
}
}
}
}
}
findings.sort_by_key(|f| (f.location.start_line, f.location.start_column));
findings.dedup_by(|a, b| {
a.location.start_line == b.location.start_line
&& a.location.start_column == b.location.start_column
});
findings
}
fn uses_flow(&self) -> bool {
true
}
}
fn should_skip_variable(name: &str) -> bool {
if name.starts_with('_') {
return true;
}
let skip_names = [
"unused", "ignore", "ignored", "dummy", "temp", "tmp", "_", "__", "err",
];
if skip_names.contains(&name) {
return true;
}
if name.len() == 1 && name.chars().next().map_or(false, |c| c.is_lowercase()) {
let meaningful = ['i', 'j', 'k', 'n', 'x', 'y', 'z'];
if !meaningful.contains(&name.chars().next().unwrap()) {
return true;
}
}
false
}
fn is_likely_global(name: &str) -> bool {
let js_globals = [
"console",
"window",
"document",
"process",
"global",
"require",
"module",
"exports",
"Buffer",
"setTimeout",
"setInterval",
"clearTimeout",
"clearInterval",
"Promise",
"fetch",
"JSON",
"Math",
"Object",
"Array",
"String",
"Number",
"Boolean",
"Date",
"Error",
"undefined",
"null",
"NaN",
"Infinity",
];
let py_builtins = [
"print",
"len",
"range",
"str",
"int",
"float",
"list",
"dict",
"set",
"tuple",
"open",
"True",
"False",
"None",
"type",
"isinstance",
"hasattr",
"getattr",
"setattr",
"super",
"self",
"cls",
];
js_globals.contains(&name) || py_builtins.contains(&name)
}
pub fn dataflow_rules() -> Vec<Box<dyn Rule>> {
vec![
Box::new(DeadStoreRule),
Box::new(UnusedVariableRule),
Box::new(CrossFunctionTaintRule),
Box::new(UninitializedVariableRule),
Box::new(super::null_pointer::NullPointerRule),
Box::new(PathTraversalTaintRule),
Box::new(CommandInjectionTaintRule),
Box::new(SqlInjectionTaintRule),
Box::new(SsrfTaintRule),
Box::new(super::xss_taint::XssDetectionRule::new()),
Box::new(super::resource_leak::ResourceLeakRule),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_skip_underscore_variables() {
assert!(should_skip_variable("_"));
assert!(should_skip_variable("_unused"));
assert!(should_skip_variable("__"));
assert!(!should_skip_variable("x"));
assert!(!should_skip_variable("data"));
}
#[test]
fn test_skip_common_unused_names() {
assert!(should_skip_variable("unused"));
assert!(should_skip_variable("ignore"));
assert!(should_skip_variable("dummy"));
assert!(should_skip_variable("err")); }
#[test]
fn test_is_likely_global() {
assert!(is_likely_global("console"));
assert!(is_likely_global("window"));
assert!(is_likely_global("print"));
assert!(is_likely_global("len"));
assert!(!is_likely_global("myVariable"));
assert!(!is_likely_global("userData"));
}
#[test]
fn test_rules_implement_trait() {
let rules = dataflow_rules();
assert!(!rules.is_empty());
for rule in &rules {
assert!(!rule.id().is_empty());
assert!(!rule.description().is_empty());
assert!(rule.uses_flow());
}
}
#[test]
fn test_path_traversal_rule_applies_to_languages() {
let rule = PathTraversalTaintRule;
assert!(rule.applies_to(Language::JavaScript));
assert!(rule.applies_to(Language::TypeScript));
assert!(rule.applies_to(Language::Python));
assert!(rule.applies_to(Language::Go));
assert!(rule.applies_to(Language::Java));
assert!(!rule.applies_to(Language::Rust));
}
#[test]
fn test_path_traversal_js_sources() {
let rule = PathTraversalTaintRule;
assert!(rule.is_path_source("req.params", Language::JavaScript));
assert!(rule.is_path_source("req.query.filename", Language::JavaScript));
assert!(rule.is_path_source("request.body", Language::JavaScript));
assert!(!rule.is_path_source("console.log", Language::JavaScript));
}
#[test]
fn test_path_traversal_python_sources() {
let rule = PathTraversalTaintRule;
assert!(rule.is_path_source("request.args", Language::Python));
assert!(rule.is_path_source("request.form", Language::Python));
assert!(rule.is_path_source("request.files", Language::Python));
assert!(!rule.is_path_source("print", Language::Python));
}
#[test]
fn test_path_traversal_go_sources() {
let rule = PathTraversalTaintRule;
assert!(rule.is_path_source("r.URL.Query", Language::Go));
assert!(rule.is_path_source("r.FormValue", Language::Go));
assert!(rule.is_path_source("c.Param", Language::Go));
assert!(!rule.is_path_source("fmt.Println", Language::Go));
}
#[test]
fn test_path_traversal_java_sources() {
let rule = PathTraversalTaintRule;
assert!(rule.is_path_source("request.getParameter", Language::Java));
assert!(rule.is_path_source("@PathVariable", Language::Java));
assert!(!rule.is_path_source("System.out", Language::Java));
}
#[test]
fn test_path_traversal_js_sinks() {
let rule = PathTraversalTaintRule;
assert!(rule.is_path_sink("fs.readFile", Language::JavaScript));
assert!(rule.is_path_sink("fs.writeFileSync", Language::JavaScript));
assert!(rule.is_path_sink("path.join", Language::JavaScript));
assert!(rule.is_path_sink("require", Language::JavaScript));
assert!(!rule.is_path_sink("console.log", Language::JavaScript));
}
#[test]
fn test_path_traversal_python_sinks() {
let rule = PathTraversalTaintRule;
assert!(rule.is_path_sink("open", Language::Python));
assert!(rule.is_path_sink("os.path.join", Language::Python));
assert!(rule.is_path_sink("pathlib.Path", Language::Python));
assert!(rule.is_path_sink("send_file", Language::Python));
assert!(!rule.is_path_sink("print", Language::Python));
}
#[test]
fn test_path_traversal_go_sinks() {
let rule = PathTraversalTaintRule;
assert!(rule.is_path_sink("os.Open", Language::Go));
assert!(rule.is_path_sink("ioutil.ReadFile", Language::Go));
assert!(rule.is_path_sink("filepath.Join", Language::Go));
assert!(rule.is_path_sink("http.ServeFile", Language::Go));
assert!(!rule.is_path_sink("fmt.Println", Language::Go));
}
#[test]
fn test_path_traversal_java_sinks() {
let rule = PathTraversalTaintRule;
assert!(rule.is_path_sink("new File", Language::Java));
assert!(rule.is_path_sink("FileInputStream", Language::Java));
assert!(rule.is_path_sink("Files.readAllBytes", Language::Java));
assert!(rule.is_path_sink("Paths.get", Language::Java));
assert!(!rule.is_path_sink("System.out.println", Language::Java));
}
#[test]
fn test_path_traversal_suggestions() {
let rule = PathTraversalTaintRule;
let js_suggestion = rule.get_suggestion(Language::JavaScript);
assert!(js_suggestion.contains("path.basename"));
assert!(js_suggestion.contains("startsWith"));
let py_suggestion = rule.get_suggestion(Language::Python);
assert!(py_suggestion.contains("os.path.basename"));
assert!(py_suggestion.contains("os.path.realpath"));
let go_suggestion = rule.get_suggestion(Language::Go);
assert!(go_suggestion.contains("filepath.Base"));
assert!(go_suggestion.contains("HasPrefix"));
let java_suggestion = rule.get_suggestion(Language::Java);
assert!(java_suggestion.contains("getCanonicalPath"));
assert!(java_suggestion.contains("FilenameUtils"));
}
#[test]
fn test_sql_injection_js_sinks() {
let rule = SqlInjectionTaintRule;
assert!(rule.is_sql_sink("db.query", Language::JavaScript));
assert!(rule.is_sql_sink("connection.execute", Language::JavaScript));
assert!(rule.is_sql_sink("mysql.query", Language::JavaScript));
assert!(rule.is_sql_sink("pool.query", Language::JavaScript));
assert!(rule.is_sql_sink("$queryRaw", Language::JavaScript));
assert!(rule.is_sql_sink("knex.raw", Language::JavaScript));
assert!(!rule.is_sql_sink("console.log", Language::JavaScript));
}
#[test]
fn test_sql_injection_python_sinks() {
let rule = SqlInjectionTaintRule;
assert!(rule.is_sql_sink("cursor.execute", Language::Python));
assert!(rule.is_sql_sink("cursor.executemany", Language::Python));
assert!(rule.is_sql_sink("session.execute", Language::Python));
assert!(rule.is_sql_sink("connection.execute", Language::Python));
assert!(rule.is_sql_sink("db.execute", Language::Python));
assert!(!rule.is_sql_sink("print", Language::Python));
}
#[test]
fn test_sql_injection_go_sinks() {
let rule = SqlInjectionTaintRule;
assert!(rule.is_sql_sink("db.Query", Language::Go));
assert!(rule.is_sql_sink("db.QueryRow", Language::Go));
assert!(rule.is_sql_sink("db.Exec", Language::Go));
assert!(rule.is_sql_sink("tx.Query", Language::Go));
assert!(rule.is_sql_sink("db.Raw", Language::Go));
assert!(!rule.is_sql_sink("fmt.Println", Language::Go));
}
#[test]
fn test_sql_injection_java_sinks() {
let rule = SqlInjectionTaintRule;
assert!(rule.is_sql_sink("Statement.executeQuery", Language::Java));
assert!(rule.is_sql_sink("Statement.executeUpdate", Language::Java));
assert!(rule.is_sql_sink("PreparedStatement.execute", Language::Java));
assert!(rule.is_sql_sink("session.createQuery", Language::Java));
assert!(rule.is_sql_sink("jdbcTemplate.query", Language::Java));
assert!(!rule.is_sql_sink("System.out.println", Language::Java));
}
#[test]
fn test_parameterized_query_detection() {
assert!(SqlInjectionTaintRule::is_parameterized_query(
"SELECT * FROM users WHERE id = ?"
));
assert!(SqlInjectionTaintRule::is_parameterized_query(
"SELECT * FROM users WHERE id = ? AND name = ?"
));
assert!(SqlInjectionTaintRule::is_parameterized_query(
"SELECT * FROM users WHERE id = $1"
));
assert!(SqlInjectionTaintRule::is_parameterized_query(
"SELECT * FROM users WHERE id = $1 AND name = $2"
));
assert!(SqlInjectionTaintRule::is_parameterized_query(
"SELECT * FROM users WHERE id = :user_id"
));
assert!(SqlInjectionTaintRule::is_parameterized_query(
"SELECT * FROM users WHERE id = :id AND name = :name"
));
assert!(SqlInjectionTaintRule::is_parameterized_query(
"SELECT * FROM users WHERE id = @userId"
));
assert!(SqlInjectionTaintRule::is_parameterized_query(
"SELECT * FROM users WHERE id = %s"
));
assert!(SqlInjectionTaintRule::is_parameterized_query(
"SELECT * FROM users WHERE id = %(user_id)s"
));
assert!(!SqlInjectionTaintRule::is_parameterized_query(
"SELECT * FROM users WHERE id = 1"
));
assert!(!SqlInjectionTaintRule::is_parameterized_query(
"SELECT * FROM users"
));
}
#[test]
fn test_sql_injection_suggestions() {
let rule = SqlInjectionTaintRule;
let js_suggestion = rule.get_suggestion(Language::JavaScript);
assert!(js_suggestion.contains("parameterized"));
assert!(js_suggestion.contains("?"));
let py_suggestion = rule.get_suggestion(Language::Python);
assert!(py_suggestion.contains("parameterized"));
assert!(py_suggestion.contains("%s"));
let go_suggestion = rule.get_suggestion(Language::Go);
assert!(go_suggestion.contains("parameterized"));
assert!(go_suggestion.contains("$1"));
let java_suggestion = rule.get_suggestion(Language::Java);
assert!(java_suggestion.contains("PreparedStatement"));
assert!(java_suggestion.contains("?"));
}
#[test]
fn test_command_injection_rule_applies_to_languages() {
let rule = CommandInjectionTaintRule;
assert!(rule.applies_to(Language::JavaScript));
assert!(rule.applies_to(Language::TypeScript));
assert!(rule.applies_to(Language::Python));
assert!(rule.applies_to(Language::Go));
assert!(rule.applies_to(Language::Rust));
assert!(rule.applies_to(Language::Java));
}
#[test]
fn test_command_injection_js_sources() {
let rule = CommandInjectionTaintRule;
assert!(rule.is_command_source("req.query", Language::JavaScript));
assert!(rule.is_command_source("req.body", Language::JavaScript));
assert!(rule.is_command_source("process.argv", Language::JavaScript));
assert!(rule.is_command_source("process.env", Language::JavaScript));
assert!(!rule.is_command_source("console.log", Language::JavaScript));
}
#[test]
fn test_command_injection_python_sources() {
let rule = CommandInjectionTaintRule;
assert!(rule.is_command_source("request.args", Language::Python));
assert!(rule.is_command_source("sys.argv", Language::Python));
assert!(rule.is_command_source("os.environ", Language::Python));
assert!(rule.is_command_source("input", Language::Python));
assert!(!rule.is_command_source("print", Language::Python));
}
#[test]
fn test_command_injection_go_sources() {
let rule = CommandInjectionTaintRule;
assert!(rule.is_command_source("r.URL.Query", Language::Go));
assert!(rule.is_command_source("os.Args", Language::Go));
assert!(rule.is_command_source("os.Getenv", Language::Go));
assert!(!rule.is_command_source("fmt.Println", Language::Go));
}
#[test]
fn test_command_injection_rust_sources() {
let rule = CommandInjectionTaintRule;
assert!(rule.is_command_source("std::env::args", Language::Rust));
assert!(rule.is_command_source("env::var", Language::Rust));
assert!(rule.is_command_source("io::stdin", Language::Rust));
assert!(!rule.is_command_source("println", Language::Rust));
}
#[test]
fn test_command_injection_java_sources() {
let rule = CommandInjectionTaintRule;
assert!(rule.is_command_source("request.getParameter", Language::Java));
assert!(rule.is_command_source("System.getenv", Language::Java));
assert!(rule.is_command_source("Scanner", Language::Java));
assert!(!rule.is_command_source("System.out", Language::Java));
}
#[test]
fn test_command_injection_js_sinks() {
let rule = CommandInjectionTaintRule;
assert!(rule.is_command_sink("child_process.exec", Language::JavaScript));
assert!(rule.is_command_sink("child_process.spawn", Language::JavaScript));
assert!(rule.is_command_sink("execa", Language::JavaScript));
assert!(!rule.is_command_sink("console.log", Language::JavaScript));
}
#[test]
fn test_command_injection_python_sinks() {
let rule = CommandInjectionTaintRule;
assert!(rule.is_command_sink("subprocess.call", Language::Python));
assert!(rule.is_command_sink("subprocess.run", Language::Python));
assert!(rule.is_command_sink("subprocess.Popen", Language::Python));
assert!(rule.is_command_sink("os.system", Language::Python));
assert!(rule.is_command_sink("os.popen", Language::Python));
assert!(!rule.is_command_sink("print", Language::Python));
}
#[test]
fn test_command_injection_go_sinks() {
let rule = CommandInjectionTaintRule;
assert!(rule.is_command_sink("exec.Command", Language::Go));
assert!(rule.is_command_sink("exec.CommandContext", Language::Go));
assert!(rule.is_command_sink("os.StartProcess", Language::Go));
assert!(!rule.is_command_sink("fmt.Println", Language::Go));
}
#[test]
fn test_command_injection_rust_sinks() {
let rule = CommandInjectionTaintRule;
assert!(rule.is_command_sink("Command::new", Language::Rust));
assert!(rule.is_command_sink("std::process::Command::new", Language::Rust));
assert!(rule.is_command_sink("tokio::process::Command::new", Language::Rust));
assert!(!rule.is_command_sink("println", Language::Rust));
}
#[test]
fn test_command_injection_java_sinks() {
let rule = CommandInjectionTaintRule;
assert!(rule.is_command_sink("Runtime.getRuntime", Language::Java));
assert!(rule.is_command_sink("ProcessBuilder", Language::Java));
assert!(!rule.is_command_sink("System.out.println", Language::Java));
}
#[test]
fn test_shell_mode_detection() {
let rule = CommandInjectionTaintRule;
assert!(rule.has_shell_mode("subprocess.call(cmd, shell=True)"));
assert!(rule.has_shell_mode("subprocess.run(cmd, shell = True)"));
assert!(rule.has_shell_mode("spawn(cmd, { shell: true })"));
assert!(rule.has_shell_mode("sh -c"));
assert!(rule.has_shell_mode("bash -c"));
assert!(rule.has_shell_mode("/bin/sh"));
assert!(rule.has_shell_mode("/bin/bash"));
assert!(!rule.has_shell_mode("subprocess.call(['ls', '-l'])"));
assert!(!rule.has_shell_mode("spawn('ls', ['-l'])"));
}
#[test]
fn test_command_injection_severity() {
let rule = CommandInjectionTaintRule;
assert_eq!(rule.determine_severity(true), Severity::Error);
assert_eq!(rule.determine_severity(false), Severity::Warning);
}
#[test]
fn test_command_injection_suggestions() {
let rule = CommandInjectionTaintRule;
let js_shell = rule.get_suggestion(Language::JavaScript, true);
assert!(js_shell.contains("CRITICAL"));
assert!(js_shell.contains("execFile") || js_shell.contains("spawn"));
let js_no_shell = rule.get_suggestion(Language::JavaScript, false);
assert!(js_no_shell.contains("array"));
let py_shell = rule.get_suggestion(Language::Python, true);
assert!(py_shell.contains("CRITICAL"));
assert!(py_shell.contains("shlex.quote"));
let py_no_shell = rule.get_suggestion(Language::Python, false);
assert!(py_no_shell.contains("list"));
let go_suggestion = rule.get_suggestion(Language::Go, false);
assert!(go_suggestion.contains("exec.Command"));
let rust_suggestion = rule.get_suggestion(Language::Rust, false);
assert!(rust_suggestion.contains("Command::new"));
assert!(rust_suggestion.contains("arg"));
let java_shell = rule.get_suggestion(Language::Java, true);
assert!(java_shell.contains("CRITICAL"));
assert!(java_shell.contains("ProcessBuilder"));
}
#[test]
fn test_ssrf_rule_applies_to_languages() {
let rule = SsrfTaintRule;
assert!(rule.applies_to(Language::JavaScript));
assert!(rule.applies_to(Language::TypeScript));
assert!(rule.applies_to(Language::Python));
assert!(rule.applies_to(Language::Go));
assert!(rule.applies_to(Language::Java));
assert!(!rule.applies_to(Language::Rust));
}
#[test]
fn test_ssrf_js_sources() {
let rule = SsrfTaintRule;
assert!(rule.is_ssrf_source("req.query.url", Language::JavaScript));
assert!(rule.is_ssrf_source("req.body.target", Language::JavaScript));
assert!(rule.is_ssrf_source("req.params.redirect", Language::JavaScript));
assert!(rule.is_ssrf_source("request.query", Language::JavaScript));
assert!(!rule.is_ssrf_source("console.log", Language::JavaScript));
}
#[test]
fn test_ssrf_python_sources() {
let rule = SsrfTaintRule;
assert!(rule.is_ssrf_source("request.args.get('url')", Language::Python));
assert!(rule.is_ssrf_source("request.form.get('target')", Language::Python));
assert!(rule.is_ssrf_source("request.json", Language::Python));
assert!(rule.is_ssrf_source("request.GET", Language::Python));
assert!(!rule.is_ssrf_source("print", Language::Python));
}
#[test]
fn test_ssrf_go_sources() {
let rule = SsrfTaintRule;
assert!(rule.is_ssrf_source("r.FormValue(\"url\")", Language::Go));
assert!(rule.is_ssrf_source("r.URL.Query", Language::Go));
assert!(rule.is_ssrf_source("c.Query(\"target\")", Language::Go));
assert!(rule.is_ssrf_source("c.Param", Language::Go));
assert!(!rule.is_ssrf_source("fmt.Println", Language::Go));
}
#[test]
fn test_ssrf_java_sources() {
let rule = SsrfTaintRule;
assert!(rule.is_ssrf_source("request.getParameter(\"url\")", Language::Java));
assert!(rule.is_ssrf_source("request.getParameter", Language::Java));
assert!(rule.is_ssrf_source("@RequestParam(\"target\")", Language::Java));
assert!(rule.is_ssrf_source("@PathVariable", Language::Java));
assert!(!rule.is_ssrf_source("System.out", Language::Java));
}
#[test]
fn test_ssrf_js_sinks() {
let rule = SsrfTaintRule;
assert!(rule.is_http_sink("fetch", Language::JavaScript));
assert!(rule.is_http_sink("axios.get", Language::JavaScript));
assert!(rule.is_http_sink("http.request", Language::JavaScript));
assert!(rule.is_http_sink("got.post", Language::JavaScript));
assert!(rule.is_http_sink("request.get", Language::JavaScript));
assert!(!rule.is_http_sink("console.log", Language::JavaScript));
}
#[test]
fn test_ssrf_python_sinks() {
let rule = SsrfTaintRule;
assert!(rule.is_http_sink("requests.get", Language::Python));
assert!(rule.is_http_sink("requests.post", Language::Python));
assert!(rule.is_http_sink("urllib.request.urlopen", Language::Python));
assert!(rule.is_http_sink("httpx.get", Language::Python));
assert!(rule.is_http_sink("aiohttp.ClientSession", Language::Python));
assert!(!rule.is_http_sink("print", Language::Python));
}
#[test]
fn test_ssrf_go_sinks() {
let rule = SsrfTaintRule;
assert!(rule.is_http_sink("http.Get", Language::Go));
assert!(rule.is_http_sink("http.Post", Language::Go));
assert!(rule.is_http_sink("http.NewRequest", Language::Go));
assert!(rule.is_http_sink("client.Do", Language::Go));
assert!(rule.is_http_sink("resty.R", Language::Go));
assert!(!rule.is_http_sink("fmt.Println", Language::Go));
}
#[test]
fn test_ssrf_java_sinks() {
let rule = SsrfTaintRule;
assert!(rule.is_http_sink("URL.openConnection", Language::Java));
assert!(rule.is_http_sink("HttpClient.execute", Language::Java));
assert!(rule.is_http_sink("RestTemplate", Language::Java));
assert!(rule.is_http_sink("restTemplate.getForObject", Language::Java));
assert!(rule.is_http_sink("WebClient", Language::Java));
assert!(rule.is_http_sink("OkHttpClient", Language::Java));
assert!(!rule.is_http_sink("System.out.println", Language::Java));
}
#[test]
fn test_ssrf_url_variable_detection() {
let rule = SsrfTaintRule;
assert!(rule.is_url_variable("targetUrl"));
assert!(rule.is_url_variable("redirectUri"));
assert!(rule.is_url_variable("callbackUrl"));
assert!(rule.is_url_variable("endpointUrl"));
assert!(rule.is_url_variable("hostAddress"));
assert!(rule.is_url_variable("srcLink"));
assert!(rule.is_url_variable("destination"));
assert!(!rule.is_url_variable("userName"));
assert!(!rule.is_url_variable("count"));
}
#[test]
fn test_ssrf_safe_url_literal() {
assert!(SsrfTaintRule::is_safe_url_literal(
"https://api.example.com/data"
));
assert!(SsrfTaintRule::is_safe_url_literal(
"http://external-service.com/webhook"
));
assert!(!SsrfTaintRule::is_safe_url_literal(
"http://127.0.0.1/admin"
));
assert!(!SsrfTaintRule::is_safe_url_literal(
"http://10.0.0.1/internal"
));
assert!(!SsrfTaintRule::is_safe_url_literal(
"http://192.168.1.1/config"
));
assert!(!SsrfTaintRule::is_safe_url_literal("http://172.16.0.1/api"));
assert!(!SsrfTaintRule::is_safe_url_literal(
"http://localhost/secret"
));
assert!(!SsrfTaintRule::is_safe_url_literal(
"http://169.254.169.254/latest/meta-data"
));
assert!(!SsrfTaintRule::is_safe_url_literal(
"http://metadata.google.internal/"
));
assert!(!SsrfTaintRule::is_safe_url_literal("file:///etc/passwd"));
assert!(!SsrfTaintRule::is_safe_url_literal("gopher://internal/"));
}
#[test]
fn test_ssrf_private_ip_patterns() {
assert!(PRIVATE_IP_PATTERNS.contains(&"127."));
assert!(PRIVATE_IP_PATTERNS.contains(&"10."));
assert!(PRIVATE_IP_PATTERNS.contains(&"192.168."));
assert!(PRIVATE_IP_PATTERNS.contains(&"169.254.169.254"));
assert!(PRIVATE_IP_PATTERNS.contains(&"localhost"));
assert!(PRIVATE_IP_PATTERNS.contains(&"metadata"));
}
#[test]
fn test_ssrf_suggestions() {
let rule = SsrfTaintRule;
let js_suggestion = rule.get_suggestion(Language::JavaScript);
assert!(js_suggestion.contains("allowlist"));
assert!(js_suggestion.contains("private IP"));
let py_suggestion = rule.get_suggestion(Language::Python);
assert!(py_suggestion.contains("ipaddress"));
assert!(py_suggestion.contains("urlparse"));
let go_suggestion = rule.get_suggestion(Language::Go);
assert!(go_suggestion.contains("net.ParseIP"));
assert!(go_suggestion.contains("url.Parse"));
let java_suggestion = rule.get_suggestion(Language::Java);
assert!(java_suggestion.contains("InetAddress"));
assert!(java_suggestion.contains("isLoopbackAddress"));
}
}