id: no-format-url
language: rust
severity: error
category: security
message: "format!() to build a URL. Use Url::parse(), .query(), or .join()."
note: "Parameter injection or path traversal risk if the input contains &, =, / or .. File paths, package names and messages that quote a URL are not reported."
rule:
kind: macro_invocation
all:
- has:
field: macro
regex: '^format$'
- has:
kind: token_tree
has:
kind: string_literal
regex: '(https?://|\{(\w+_)?(url|uri|host|hostname|endpoint)(:[^}]*)?\}/|/[A-Za-z][\w.-]*/\{(\w+_)?(id|uid|uuid|slug)(:[^}]*)?\})'
not:
regex: '\s'
ignores:
- "**/tests/**"
- "**/*_tests/**"
- "**/*_test/**"
- "**/test_*"
- "**/*_test.rs"
- "**/fixtures/**"
skip_test_code: true
tests:
should_match:
- 'let url = format!("https://api.example.com/users/{}", user_id);'
- 'let url = format!("{base_url}/items/{id}");'
- 'let path = format!("/users/{user_id}/posts");'
- 'let url = format!("{host}/api/v1{path}");'
- 'let url = format!("{api_url:?}/search");'
should_not_match:
- 'let msg = format!("connected to {}", host);'
- 'let label = format!("user-{}", count);'
- 'let package = format!("@{scope}/{name}");'
- 'let path = format!("{dir}/{filename}");'
- 'let manifest = format!("{root}/{crate_name}/Cargo.toml");'
- 'let ratio = format!("{width}/100");'
- 'let proc_status = format!("/proc/{pid}/status");'
- 'let feature = format!("{dep_name}/{dep_feature}");'
- 'let msg = format!("See https://example.com/issues/{issue} for details.");'