fix: escape LIKE metacharacters in project resolver
User-supplied project names containing `%` or `_` were passed directly into LIKE patterns, causing unintended wildcard matching. For example, `my_project` would match `my-project` because `_` is a single-char wildcard in SQL LIKE. Added escape_like() helper that escapes `\`, `%`, and `_` with backslash, and added ESCAPE '\' clauses to both the suffix-match and substring-match queries in resolve_project(). Includes two regression tests: - test_underscore_not_wildcard: `_` in input must not match `-` - test_percent_not_wildcard: `%` in input must not match arbitrary strings
This commit is contained in:
@@ -21,13 +21,14 @@ pub fn resolve_project(conn: &Connection, project_str: &str) -> Result<i64> {
|
||||
return Ok(id);
|
||||
}
|
||||
|
||||
let escaped = escape_like(project_str);
|
||||
let mut suffix_stmt = conn.prepare(
|
||||
"SELECT id, path_with_namespace FROM projects
|
||||
WHERE path_with_namespace LIKE '%/' || ?1
|
||||
OR path_with_namespace = ?1",
|
||||
WHERE path_with_namespace LIKE '%/' || ?1 ESCAPE '\\'
|
||||
OR path_with_namespace = ?2",
|
||||
)?;
|
||||
let suffix_matches: Vec<(i64, String)> = suffix_stmt
|
||||
.query_map(rusqlite::params![project_str], |row| {
|
||||
.query_map(rusqlite::params![escaped, project_str], |row| {
|
||||
Ok((row.get(0)?, row.get(1)?))
|
||||
})?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
@@ -52,10 +53,10 @@ pub fn resolve_project(conn: &Connection, project_str: &str) -> Result<i64> {
|
||||
|
||||
let mut substr_stmt = conn.prepare(
|
||||
"SELECT id, path_with_namespace FROM projects
|
||||
WHERE LOWER(path_with_namespace) LIKE '%' || LOWER(?1) || '%'",
|
||||
WHERE LOWER(path_with_namespace) LIKE '%' || LOWER(?1) || '%' ESCAPE '\\'",
|
||||
)?;
|
||||
let substr_matches: Vec<(i64, String)> = substr_stmt
|
||||
.query_map(rusqlite::params![project_str], |row| {
|
||||
.query_map(rusqlite::params![escaped], |row| {
|
||||
Ok((row.get(0)?, row.get(1)?))
|
||||
})?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
@@ -103,6 +104,15 @@ pub fn resolve_project(conn: &Connection, project_str: &str) -> Result<i64> {
|
||||
)))
|
||||
}
|
||||
|
||||
/// Escape LIKE metacharacters so `%` and `_` in user input are treated as
|
||||
/// literals. All queries using this must include `ESCAPE '\'`.
|
||||
fn escape_like(input: &str) -> String {
|
||||
input
|
||||
.replace('\\', "\\\\")
|
||||
.replace('%', "\\%")
|
||||
.replace('_', "\\_")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -241,4 +251,24 @@ mod tests {
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("No projects have been synced"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_underscore_not_wildcard() {
|
||||
let conn = setup_db();
|
||||
insert_project(&conn, 1, "backend/my_project");
|
||||
insert_project(&conn, 2, "backend/my-project");
|
||||
// `_` in user input must not match `-` (LIKE wildcard behavior)
|
||||
let id = resolve_project(&conn, "my_project").unwrap();
|
||||
assert_eq!(id, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_percent_not_wildcard() {
|
||||
let conn = setup_db();
|
||||
insert_project(&conn, 1, "backend/a%b");
|
||||
insert_project(&conn, 2, "backend/axyzb");
|
||||
// `%` in user input must not match arbitrary strings
|
||||
let id = resolve_project(&conn, "a%b").unwrap();
|
||||
assert_eq!(id, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user