mirror of https://github.com/astral-sh/ruff
Avoid false-positives for list concatenations in SQL construction (#12720)
## Summary Closes https://github.com/astral-sh/ruff/issues/12710.
This commit is contained in:
parent
aae9619d3d
commit
90e5bc2bd9
|
|
@ -102,3 +102,11 @@ query = "REPLACE INTO table VALUES (%s)" % (var,)
|
||||||
query = "REPLACE table VALUES (%s)" % (var,)
|
query = "REPLACE table VALUES (%s)" % (var,)
|
||||||
|
|
||||||
query = "Deselect something that is not SQL even though it has a ' from ' somewhere in %s." % "there"
|
query = "Deselect something that is not SQL even though it has a ' from ' somewhere in %s." % "there"
|
||||||
|
|
||||||
|
# # pass
|
||||||
|
["select colA from tableA"] + ["select colB from tableB"]
|
||||||
|
"SELECT * FROM " + (["table1"] if x > 0 else ["table2"])
|
||||||
|
|
||||||
|
# # errors
|
||||||
|
"SELECT * FROM " + ("table1" if x > 0 else "table2")
|
||||||
|
"SELECT * FROM " + ("table1" if x > 0 else ["table2"])
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ use ruff_python_ast::{self as ast, Expr, Operator};
|
||||||
|
|
||||||
use ruff_diagnostics::{Diagnostic, Violation};
|
use ruff_diagnostics::{Diagnostic, Violation};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
use ruff_python_ast::helpers::any_over_expr;
|
|
||||||
use ruff_python_ast::str::raw_contents;
|
use ruff_python_ast::str::raw_contents;
|
||||||
use ruff_source_file::Locator;
|
use ruff_source_file::Locator;
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
|
@ -45,25 +44,6 @@ impl Violation for HardcodedSQLExpression {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Concatenates the contents of an f-string, without the prefix and quotes,
|
|
||||||
/// and escapes any special characters.
|
|
||||||
///
|
|
||||||
/// ## Example
|
|
||||||
///
|
|
||||||
/// ```python
|
|
||||||
/// "foo" f"bar {x}" "baz"
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// becomes `foobar {x}baz`.
|
|
||||||
fn concatenated_f_string(expr: &ast::ExprFString, locator: &Locator) -> String {
|
|
||||||
expr.value
|
|
||||||
.iter()
|
|
||||||
.filter_map(|part| {
|
|
||||||
raw_contents(locator.slice(part)).map(|s| s.escape_default().to_string())
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// S608
|
/// S608
|
||||||
pub(crate) fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) {
|
pub(crate) fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) {
|
||||||
let content = match expr {
|
let content = match expr {
|
||||||
|
|
@ -79,7 +59,7 @@ pub(crate) fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) {
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if !any_over_expr(expr, &Expr::is_string_literal_expr) {
|
if is_explicit_concatenation(expr) != Some(true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
checker.generator().expr(expr)
|
checker.generator().expr(expr)
|
||||||
|
|
@ -119,3 +99,98 @@ pub(crate) fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) {
|
||||||
.push(Diagnostic::new(HardcodedSQLExpression, expr.range()));
|
.push(Diagnostic::new(HardcodedSQLExpression, expr.range()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Concatenates the contents of an f-string, without the prefix and quotes,
|
||||||
|
/// and escapes any special characters.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// "foo" f"bar {x}" "baz"
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// becomes `foobar {x}baz`.
|
||||||
|
fn concatenated_f_string(expr: &ast::ExprFString, locator: &Locator) -> String {
|
||||||
|
expr.value
|
||||||
|
.iter()
|
||||||
|
.filter_map(|part| {
|
||||||
|
raw_contents(locator.slice(part)).map(|s| s.escape_default().to_string())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `Some(true)` if an expression appears to be an explicit string concatenation,
|
||||||
|
/// `Some(false)` if it's _not_ an explicit concatenation, and `None` if it's ambiguous.
|
||||||
|
fn is_explicit_concatenation(expr: &Expr) -> Option<bool> {
|
||||||
|
match expr {
|
||||||
|
Expr::BinOp(ast::ExprBinOp { left, right, .. }) => {
|
||||||
|
let left = is_explicit_concatenation(left);
|
||||||
|
let right = is_explicit_concatenation(right);
|
||||||
|
match (left, right) {
|
||||||
|
// If either side is definitively _not_ a string, neither is the expression.
|
||||||
|
(Some(false), _) | (_, Some(false)) => Some(false),
|
||||||
|
// If either side is definitively a string, the expression is a string.
|
||||||
|
(Some(true), _) | (_, Some(true)) => Some(true),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ambiguous (e.g., `x + y`).
|
||||||
|
Expr::Call(_) => None,
|
||||||
|
Expr::Subscript(_) => None,
|
||||||
|
Expr::Attribute(_) => None,
|
||||||
|
Expr::Name(_) => None,
|
||||||
|
|
||||||
|
// Non-strings.
|
||||||
|
Expr::Lambda(_) => Some(false),
|
||||||
|
Expr::List(_) => Some(false),
|
||||||
|
Expr::Tuple(_) => Some(false),
|
||||||
|
Expr::Dict(_) => Some(false),
|
||||||
|
Expr::Set(_) => Some(false),
|
||||||
|
Expr::Generator(_) => Some(false),
|
||||||
|
Expr::Yield(_) => Some(false),
|
||||||
|
Expr::YieldFrom(_) => Some(false),
|
||||||
|
Expr::Await(_) => Some(false),
|
||||||
|
Expr::Starred(_) => Some(false),
|
||||||
|
Expr::Slice(_) => Some(false),
|
||||||
|
Expr::BooleanLiteral(_) => Some(false),
|
||||||
|
Expr::EllipsisLiteral(_) => Some(false),
|
||||||
|
Expr::NumberLiteral(_) => Some(false),
|
||||||
|
Expr::ListComp(_) => Some(false),
|
||||||
|
Expr::SetComp(_) => Some(false),
|
||||||
|
Expr::DictComp(_) => Some(false),
|
||||||
|
Expr::Compare(_) => Some(false),
|
||||||
|
Expr::FString(_) => Some(true),
|
||||||
|
Expr::StringLiteral(_) => Some(true),
|
||||||
|
Expr::BytesLiteral(_) => Some(false),
|
||||||
|
Expr::NoneLiteral(_) => Some(false),
|
||||||
|
Expr::IpyEscapeCommand(_) => Some(false),
|
||||||
|
|
||||||
|
// Conditionally strings.
|
||||||
|
Expr::Named(ast::ExprNamed { value, .. }) => is_explicit_concatenation(value),
|
||||||
|
Expr::If(ast::ExprIf { body, orelse, .. }) => {
|
||||||
|
let body = is_explicit_concatenation(body);
|
||||||
|
let orelse = is_explicit_concatenation(orelse);
|
||||||
|
match (body, orelse) {
|
||||||
|
// If either side is definitively a string, the expression could be a string.
|
||||||
|
(Some(true), _) | (_, Some(true)) => Some(true),
|
||||||
|
// If both sides are definitively _not_ a string, neither is the expression.
|
||||||
|
(Some(false), Some(false)) => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::BoolOp(ast::ExprBoolOp { values, .. }) => {
|
||||||
|
let values = values
|
||||||
|
.iter()
|
||||||
|
.map(is_explicit_concatenation)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if values.iter().any(|v| *v == Some(true)) {
|
||||||
|
Some(true)
|
||||||
|
} else if values.iter().all(|v| *v == Some(false)) {
|
||||||
|
Some(false)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => is_explicit_concatenation(operand),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -479,4 +479,18 @@ S608.py:102:9: S608 Possible SQL injection vector through string-based query con
|
||||||
104 | query = "Deselect something that is not SQL even though it has a ' from ' somewhere in %s." % "there"
|
104 | query = "Deselect something that is not SQL even though it has a ' from ' somewhere in %s." % "there"
|
||||||
|
|
|
|
||||||
|
|
||||||
|
S608.py:111:1: S608 Possible SQL injection vector through string-based query construction
|
||||||
|
|
|
||||||
|
110 | # # errors
|
||||||
|
111 | "SELECT * FROM " + ("table1" if x > 0 else "table2")
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S608
|
||||||
|
112 | "SELECT * FROM " + ("table1" if x > 0 else ["table2"])
|
||||||
|
|
|
||||||
|
|
||||||
|
S608.py:112:1: S608 Possible SQL injection vector through string-based query construction
|
||||||
|
|
|
||||||
|
110 | # # errors
|
||||||
|
111 | "SELECT * FROM " + ("table1" if x > 0 else "table2")
|
||||||
|
112 | "SELECT * FROM " + ("table1" if x > 0 else ["table2"])
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S608
|
||||||
|
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue