[flake8-bandit] Detect httpx for S113 (#12174)

## Summary

Bandit now also reports `B113` on `httpx`
(https://github.com/PyCQA/bandit/pull/1060). This PR implements the same
logic, to detect missing or `None` timeouts for `httpx` alongside
`requests`.

## Test Plan

Snapshot tests.
This commit is contained in:
Mathieu Kniewallner
2024-07-04 01:26:55 +02:00
committed by GitHub
parent a184f84f69
commit 8210c1ed5b
3 changed files with 411 additions and 145 deletions

View File

@@ -1,23 +1,71 @@
import httpx
import requests
# OK
requests.get('https://gmail.com', timeout=5)
requests.post('https://gmail.com', timeout=5)
requests.put('https://gmail.com', timeout=5)
requests.delete('https://gmail.com', timeout=5)
requests.patch('https://gmail.com', timeout=5)
requests.options('https://gmail.com', timeout=5)
requests.head('https://gmail.com', timeout=5)
httpx.get('https://gmail.com', timeout=5)
httpx.post('https://gmail.com', timeout=5)
httpx.put('https://gmail.com', timeout=5)
httpx.delete('https://gmail.com', timeout=5)
httpx.patch('https://gmail.com', timeout=5)
httpx.options('https://gmail.com', timeout=5)
httpx.head('https://gmail.com', timeout=5)
httpx.Client(timeout=5)
httpx.AsyncClient(timeout=5)
with httpx.Client(timeout=5) as client:
client.get('https://gmail.com')
async def foo():
async with httpx.AsyncClient(timeout=5) as client:
await client.get('https://gmail.com')
# Errors
requests.get('https://gmail.com')
requests.get('https://gmail.com', timeout=None)
requests.get('https://gmail.com', timeout=5)
requests.post('https://gmail.com')
requests.post('https://gmail.com', timeout=None)
requests.post('https://gmail.com', timeout=5)
requests.put('https://gmail.com')
requests.put('https://gmail.com', timeout=None)
requests.put('https://gmail.com', timeout=5)
requests.delete('https://gmail.com')
requests.delete('https://gmail.com', timeout=None)
requests.delete('https://gmail.com', timeout=5)
requests.patch('https://gmail.com')
requests.patch('https://gmail.com', timeout=None)
requests.patch('https://gmail.com', timeout=5)
requests.options('https://gmail.com')
requests.options('https://gmail.com', timeout=None)
requests.options('https://gmail.com', timeout=5)
requests.head('https://gmail.com')
requests.head('https://gmail.com', timeout=None)
requests.head('https://gmail.com', timeout=5)
httpx.get('https://gmail.com')
httpx.get('https://gmail.com', timeout=None)
httpx.post('https://gmail.com')
httpx.post('https://gmail.com', timeout=None)
httpx.put('https://gmail.com')
httpx.put('https://gmail.com', timeout=None)
httpx.delete('https://gmail.com')
httpx.delete('https://gmail.com', timeout=None)
httpx.patch('https://gmail.com')
httpx.patch('https://gmail.com', timeout=None)
httpx.options('https://gmail.com')
httpx.options('https://gmail.com', timeout=None)
httpx.head('https://gmail.com')
httpx.head('https://gmail.com', timeout=None)
httpx.Client()
httpx.Client(timeout=None)
httpx.AsyncClient()
httpx.AsyncClient(timeout=None)
with httpx.Client() as client:
client.get('https://gmail.com')
with httpx.Client(timeout=None) as client:
client.get('https://gmail.com')
async def bar():
async with httpx.AsyncClient() as client:
await client.get('https://gmail.com')
async def baz():
async with httpx.AsyncClient(timeout=None) as client:
await client.get('https://gmail.com')

View File

@@ -7,8 +7,8 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of the Python `requests` module that omit the `timeout`
/// parameter.
/// Checks for uses of the Python `requests` or `httpx` module that omit the
/// `timeout` parameter.
///
/// ## Why is this bad?
/// The `timeout` parameter is used to set the maximum time to wait for a
@@ -31,48 +31,50 @@ use crate::checkers::ast::Checker;
///
/// ## References
/// - [Requests documentation: Timeouts](https://requests.readthedocs.io/en/latest/user/advanced/#timeouts)
/// - [httpx documentation: Timeouts](https://www.python-httpx.org/advanced/timeouts/)
#[violation]
pub struct RequestWithoutTimeout {
implicit: bool,
module: String,
}
impl Violation for RequestWithoutTimeout {
#[derive_message_formats]
fn message(&self) -> String {
let RequestWithoutTimeout { implicit } = self;
let RequestWithoutTimeout { implicit, module } = self;
if *implicit {
format!("Probable use of requests call without timeout")
format!("Probable use of `{module}` call without timeout")
} else {
format!("Probable use of requests call with timeout set to `None`")
format!("Probable use of `{module}` call with timeout set to `None`")
}
}
}
/// S113
pub(crate) fn request_without_timeout(checker: &mut Checker, call: &ast::ExprCall) {
if checker
if let Some(module) = checker
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
[
"requests",
"get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request"
]
)
.and_then(|qualified_name| match qualified_name.segments() {
["requests", "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request"] => {
Some("requests")
}
["httpx", "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request" | "stream" | "Client" | "AsyncClient"] => {
Some("httpx")
}
_ => None,
})
{
if let Some(keyword) = call.arguments.find_keyword("timeout") {
if keyword.value.is_none_literal_expr() {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { implicit: false },
RequestWithoutTimeout { implicit: false, module: module.to_string() },
keyword.range(),
));
}
} else {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { implicit: true },
RequestWithoutTimeout { implicit: true, module: module.to_string() },
call.func.range(),
));
}

View File

@@ -1,142 +1,358 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S113.py:3:1: S113 Probable use of requests call without timeout
|
1 | import requests
2 |
3 | requests.get('https://gmail.com')
| ^^^^^^^^^^^^ S113
4 | requests.get('https://gmail.com', timeout=None)
5 | requests.get('https://gmail.com', timeout=5)
|
S113.py:4:35: S113 Probable use of requests call with timeout set to `None`
|
3 | requests.get('https://gmail.com')
4 | requests.get('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
5 | requests.get('https://gmail.com', timeout=5)
6 | requests.post('https://gmail.com')
|
S113.py:6:1: S113 Probable use of requests call without timeout
|
4 | requests.get('https://gmail.com', timeout=None)
5 | requests.get('https://gmail.com', timeout=5)
6 | requests.post('https://gmail.com')
| ^^^^^^^^^^^^^ S113
7 | requests.post('https://gmail.com', timeout=None)
8 | requests.post('https://gmail.com', timeout=5)
|
S113.py:7:36: S113 Probable use of requests call with timeout set to `None`
|
5 | requests.get('https://gmail.com', timeout=5)
6 | requests.post('https://gmail.com')
7 | requests.post('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
8 | requests.post('https://gmail.com', timeout=5)
9 | requests.put('https://gmail.com')
|
S113.py:9:1: S113 Probable use of requests call without timeout
S113.py:29:1: S113 Probable use of `requests` call without timeout
|
7 | requests.post('https://gmail.com', timeout=None)
8 | requests.post('https://gmail.com', timeout=5)
9 | requests.put('https://gmail.com')
28 | # Errors
29 | requests.get('https://gmail.com')
| ^^^^^^^^^^^^ S113
10 | requests.put('https://gmail.com', timeout=None)
11 | requests.put('https://gmail.com', timeout=5)
30 | requests.get('https://gmail.com', timeout=None)
31 | requests.post('https://gmail.com')
|
S113.py:10:35: S113 Probable use of requests call with timeout set to `None`
S113.py:30:35: S113 Probable use of `requests` call with timeout set to `None`
|
8 | requests.post('https://gmail.com', timeout=5)
9 | requests.put('https://gmail.com')
10 | requests.put('https://gmail.com', timeout=None)
28 | # Errors
29 | requests.get('https://gmail.com')
30 | requests.get('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
11 | requests.put('https://gmail.com', timeout=5)
12 | requests.delete('https://gmail.com')
31 | requests.post('https://gmail.com')
32 | requests.post('https://gmail.com', timeout=None)
|
S113.py:12:1: S113 Probable use of requests call without timeout
S113.py:31:1: S113 Probable use of `requests` call without timeout
|
10 | requests.put('https://gmail.com', timeout=None)
11 | requests.put('https://gmail.com', timeout=5)
12 | requests.delete('https://gmail.com')
| ^^^^^^^^^^^^^^^ S113
13 | requests.delete('https://gmail.com', timeout=None)
14 | requests.delete('https://gmail.com', timeout=5)
|
S113.py:13:38: S113 Probable use of requests call with timeout set to `None`
|
11 | requests.put('https://gmail.com', timeout=5)
12 | requests.delete('https://gmail.com')
13 | requests.delete('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
14 | requests.delete('https://gmail.com', timeout=5)
15 | requests.patch('https://gmail.com')
|
S113.py:15:1: S113 Probable use of requests call without timeout
|
13 | requests.delete('https://gmail.com', timeout=None)
14 | requests.delete('https://gmail.com', timeout=5)
15 | requests.patch('https://gmail.com')
| ^^^^^^^^^^^^^^ S113
16 | requests.patch('https://gmail.com', timeout=None)
17 | requests.patch('https://gmail.com', timeout=5)
|
S113.py:16:37: S113 Probable use of requests call with timeout set to `None`
|
14 | requests.delete('https://gmail.com', timeout=5)
15 | requests.patch('https://gmail.com')
16 | requests.patch('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
17 | requests.patch('https://gmail.com', timeout=5)
18 | requests.options('https://gmail.com')
|
S113.py:18:1: S113 Probable use of requests call without timeout
|
16 | requests.patch('https://gmail.com', timeout=None)
17 | requests.patch('https://gmail.com', timeout=5)
18 | requests.options('https://gmail.com')
| ^^^^^^^^^^^^^^^^ S113
19 | requests.options('https://gmail.com', timeout=None)
20 | requests.options('https://gmail.com', timeout=5)
|
S113.py:19:39: S113 Probable use of requests call with timeout set to `None`
|
17 | requests.patch('https://gmail.com', timeout=5)
18 | requests.options('https://gmail.com')
19 | requests.options('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
20 | requests.options('https://gmail.com', timeout=5)
21 | requests.head('https://gmail.com')
|
S113.py:21:1: S113 Probable use of requests call without timeout
|
19 | requests.options('https://gmail.com', timeout=None)
20 | requests.options('https://gmail.com', timeout=5)
21 | requests.head('https://gmail.com')
29 | requests.get('https://gmail.com')
30 | requests.get('https://gmail.com', timeout=None)
31 | requests.post('https://gmail.com')
| ^^^^^^^^^^^^^ S113
22 | requests.head('https://gmail.com', timeout=None)
23 | requests.head('https://gmail.com', timeout=5)
32 | requests.post('https://gmail.com', timeout=None)
33 | requests.put('https://gmail.com')
|
S113.py:22:36: S113 Probable use of requests call with timeout set to `None`
S113.py:32:36: S113 Probable use of `requests` call with timeout set to `None`
|
20 | requests.options('https://gmail.com', timeout=5)
21 | requests.head('https://gmail.com')
22 | requests.head('https://gmail.com', timeout=None)
30 | requests.get('https://gmail.com', timeout=None)
31 | requests.post('https://gmail.com')
32 | requests.post('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
23 | requests.head('https://gmail.com', timeout=5)
33 | requests.put('https://gmail.com')
34 | requests.put('https://gmail.com', timeout=None)
|
S113.py:33:1: S113 Probable use of `requests` call without timeout
|
31 | requests.post('https://gmail.com')
32 | requests.post('https://gmail.com', timeout=None)
33 | requests.put('https://gmail.com')
| ^^^^^^^^^^^^ S113
34 | requests.put('https://gmail.com', timeout=None)
35 | requests.delete('https://gmail.com')
|
S113.py:34:35: S113 Probable use of `requests` call with timeout set to `None`
|
32 | requests.post('https://gmail.com', timeout=None)
33 | requests.put('https://gmail.com')
34 | requests.put('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
35 | requests.delete('https://gmail.com')
36 | requests.delete('https://gmail.com', timeout=None)
|
S113.py:35:1: S113 Probable use of `requests` call without timeout
|
33 | requests.put('https://gmail.com')
34 | requests.put('https://gmail.com', timeout=None)
35 | requests.delete('https://gmail.com')
| ^^^^^^^^^^^^^^^ S113
36 | requests.delete('https://gmail.com', timeout=None)
37 | requests.patch('https://gmail.com')
|
S113.py:36:38: S113 Probable use of `requests` call with timeout set to `None`
|
34 | requests.put('https://gmail.com', timeout=None)
35 | requests.delete('https://gmail.com')
36 | requests.delete('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
37 | requests.patch('https://gmail.com')
38 | requests.patch('https://gmail.com', timeout=None)
|
S113.py:37:1: S113 Probable use of `requests` call without timeout
|
35 | requests.delete('https://gmail.com')
36 | requests.delete('https://gmail.com', timeout=None)
37 | requests.patch('https://gmail.com')
| ^^^^^^^^^^^^^^ S113
38 | requests.patch('https://gmail.com', timeout=None)
39 | requests.options('https://gmail.com')
|
S113.py:38:37: S113 Probable use of `requests` call with timeout set to `None`
|
36 | requests.delete('https://gmail.com', timeout=None)
37 | requests.patch('https://gmail.com')
38 | requests.patch('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
39 | requests.options('https://gmail.com')
40 | requests.options('https://gmail.com', timeout=None)
|
S113.py:39:1: S113 Probable use of `requests` call without timeout
|
37 | requests.patch('https://gmail.com')
38 | requests.patch('https://gmail.com', timeout=None)
39 | requests.options('https://gmail.com')
| ^^^^^^^^^^^^^^^^ S113
40 | requests.options('https://gmail.com', timeout=None)
41 | requests.head('https://gmail.com')
|
S113.py:40:39: S113 Probable use of `requests` call with timeout set to `None`
|
38 | requests.patch('https://gmail.com', timeout=None)
39 | requests.options('https://gmail.com')
40 | requests.options('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
41 | requests.head('https://gmail.com')
42 | requests.head('https://gmail.com', timeout=None)
|
S113.py:41:1: S113 Probable use of `requests` call without timeout
|
39 | requests.options('https://gmail.com')
40 | requests.options('https://gmail.com', timeout=None)
41 | requests.head('https://gmail.com')
| ^^^^^^^^^^^^^ S113
42 | requests.head('https://gmail.com', timeout=None)
|
S113.py:42:36: S113 Probable use of `requests` call with timeout set to `None`
|
40 | requests.options('https://gmail.com', timeout=None)
41 | requests.head('https://gmail.com')
42 | requests.head('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
43 |
44 | httpx.get('https://gmail.com')
|
S113.py:44:1: S113 Probable use of `httpx` call without timeout
|
42 | requests.head('https://gmail.com', timeout=None)
43 |
44 | httpx.get('https://gmail.com')
| ^^^^^^^^^ S113
45 | httpx.get('https://gmail.com', timeout=None)
46 | httpx.post('https://gmail.com')
|
S113.py:45:32: S113 Probable use of `httpx` call with timeout set to `None`
|
44 | httpx.get('https://gmail.com')
45 | httpx.get('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
46 | httpx.post('https://gmail.com')
47 | httpx.post('https://gmail.com', timeout=None)
|
S113.py:46:1: S113 Probable use of `httpx` call without timeout
|
44 | httpx.get('https://gmail.com')
45 | httpx.get('https://gmail.com', timeout=None)
46 | httpx.post('https://gmail.com')
| ^^^^^^^^^^ S113
47 | httpx.post('https://gmail.com', timeout=None)
48 | httpx.put('https://gmail.com')
|
S113.py:47:33: S113 Probable use of `httpx` call with timeout set to `None`
|
45 | httpx.get('https://gmail.com', timeout=None)
46 | httpx.post('https://gmail.com')
47 | httpx.post('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
48 | httpx.put('https://gmail.com')
49 | httpx.put('https://gmail.com', timeout=None)
|
S113.py:48:1: S113 Probable use of `httpx` call without timeout
|
46 | httpx.post('https://gmail.com')
47 | httpx.post('https://gmail.com', timeout=None)
48 | httpx.put('https://gmail.com')
| ^^^^^^^^^ S113
49 | httpx.put('https://gmail.com', timeout=None)
50 | httpx.delete('https://gmail.com')
|
S113.py:49:32: S113 Probable use of `httpx` call with timeout set to `None`
|
47 | httpx.post('https://gmail.com', timeout=None)
48 | httpx.put('https://gmail.com')
49 | httpx.put('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
50 | httpx.delete('https://gmail.com')
51 | httpx.delete('https://gmail.com', timeout=None)
|
S113.py:50:1: S113 Probable use of `httpx` call without timeout
|
48 | httpx.put('https://gmail.com')
49 | httpx.put('https://gmail.com', timeout=None)
50 | httpx.delete('https://gmail.com')
| ^^^^^^^^^^^^ S113
51 | httpx.delete('https://gmail.com', timeout=None)
52 | httpx.patch('https://gmail.com')
|
S113.py:51:35: S113 Probable use of `httpx` call with timeout set to `None`
|
49 | httpx.put('https://gmail.com', timeout=None)
50 | httpx.delete('https://gmail.com')
51 | httpx.delete('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
52 | httpx.patch('https://gmail.com')
53 | httpx.patch('https://gmail.com', timeout=None)
|
S113.py:52:1: S113 Probable use of `httpx` call without timeout
|
50 | httpx.delete('https://gmail.com')
51 | httpx.delete('https://gmail.com', timeout=None)
52 | httpx.patch('https://gmail.com')
| ^^^^^^^^^^^ S113
53 | httpx.patch('https://gmail.com', timeout=None)
54 | httpx.options('https://gmail.com')
|
S113.py:53:34: S113 Probable use of `httpx` call with timeout set to `None`
|
51 | httpx.delete('https://gmail.com', timeout=None)
52 | httpx.patch('https://gmail.com')
53 | httpx.patch('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
54 | httpx.options('https://gmail.com')
55 | httpx.options('https://gmail.com', timeout=None)
|
S113.py:54:1: S113 Probable use of `httpx` call without timeout
|
52 | httpx.patch('https://gmail.com')
53 | httpx.patch('https://gmail.com', timeout=None)
54 | httpx.options('https://gmail.com')
| ^^^^^^^^^^^^^ S113
55 | httpx.options('https://gmail.com', timeout=None)
56 | httpx.head('https://gmail.com')
|
S113.py:55:36: S113 Probable use of `httpx` call with timeout set to `None`
|
53 | httpx.patch('https://gmail.com', timeout=None)
54 | httpx.options('https://gmail.com')
55 | httpx.options('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
56 | httpx.head('https://gmail.com')
57 | httpx.head('https://gmail.com', timeout=None)
|
S113.py:56:1: S113 Probable use of `httpx` call without timeout
|
54 | httpx.options('https://gmail.com')
55 | httpx.options('https://gmail.com', timeout=None)
56 | httpx.head('https://gmail.com')
| ^^^^^^^^^^ S113
57 | httpx.head('https://gmail.com', timeout=None)
58 | httpx.Client()
|
S113.py:57:33: S113 Probable use of `httpx` call with timeout set to `None`
|
55 | httpx.options('https://gmail.com', timeout=None)
56 | httpx.head('https://gmail.com')
57 | httpx.head('https://gmail.com', timeout=None)
| ^^^^^^^^^^^^ S113
58 | httpx.Client()
59 | httpx.Client(timeout=None)
|
S113.py:58:1: S113 Probable use of `httpx` call without timeout
|
56 | httpx.head('https://gmail.com')
57 | httpx.head('https://gmail.com', timeout=None)
58 | httpx.Client()
| ^^^^^^^^^^^^ S113
59 | httpx.Client(timeout=None)
60 | httpx.AsyncClient()
|
S113.py:59:14: S113 Probable use of `httpx` call with timeout set to `None`
|
57 | httpx.head('https://gmail.com', timeout=None)
58 | httpx.Client()
59 | httpx.Client(timeout=None)
| ^^^^^^^^^^^^ S113
60 | httpx.AsyncClient()
61 | httpx.AsyncClient(timeout=None)
|
S113.py:60:1: S113 Probable use of `httpx` call without timeout
|
58 | httpx.Client()
59 | httpx.Client(timeout=None)
60 | httpx.AsyncClient()
| ^^^^^^^^^^^^^^^^^ S113
61 | httpx.AsyncClient(timeout=None)
62 | with httpx.Client() as client:
|
S113.py:61:19: S113 Probable use of `httpx` call with timeout set to `None`
|
59 | httpx.Client(timeout=None)
60 | httpx.AsyncClient()
61 | httpx.AsyncClient(timeout=None)
| ^^^^^^^^^^^^ S113
62 | with httpx.Client() as client:
63 | client.get('https://gmail.com')
|
S113.py:62:6: S113 Probable use of `httpx` call without timeout
|
60 | httpx.AsyncClient()
61 | httpx.AsyncClient(timeout=None)
62 | with httpx.Client() as client:
| ^^^^^^^^^^^^ S113
63 | client.get('https://gmail.com')
64 | with httpx.Client(timeout=None) as client:
|
S113.py:64:19: S113 Probable use of `httpx` call with timeout set to `None`
|
62 | with httpx.Client() as client:
63 | client.get('https://gmail.com')
64 | with httpx.Client(timeout=None) as client:
| ^^^^^^^^^^^^ S113
65 | client.get('https://gmail.com')
66 | async def bar():
|
S113.py:67:16: S113 Probable use of `httpx` call without timeout
|
65 | client.get('https://gmail.com')
66 | async def bar():
67 | async with httpx.AsyncClient() as client:
| ^^^^^^^^^^^^^^^^^ S113
68 | await client.get('https://gmail.com')
69 | async def baz():
|
S113.py:70:34: S113 Probable use of `httpx` call with timeout set to `None`
|
68 | await client.get('https://gmail.com')
69 | async def baz():
70 | async with httpx.AsyncClient(timeout=None) as client:
| ^^^^^^^^^^^^ S113
71 | await client.get('https://gmail.com')
|