1
1
//! Check for calls to suspicious functions, or calls into suspicious modules.
2
2
//!
3
3
//! See: <https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html>
4
+ use itertools:: Either ;
4
5
use ruff_diagnostics:: { Diagnostic , DiagnosticKind , Violation } ;
5
6
use ruff_macros:: { derive_message_formats, violation} ;
6
- use ruff_python_ast:: { self as ast, Decorator , Expr , ExprCall } ;
7
+ use ruff_python_ast:: { self as ast, Decorator , Expr , ExprCall , Operator } ;
7
8
use ruff_text_size:: Ranged ;
8
9
9
10
use crate :: checkers:: ast:: Checker ;
@@ -838,6 +839,43 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
838
839
true
839
840
}
840
841
842
+ /// Returns `true` if the iterator starts with an HTTP or HTTPS prefix.
843
+ fn has_http_prefix ( chars : impl Iterator < Item = char > + Clone ) -> bool {
844
+ has_prefix ( chars. clone ( ) . skip_while ( |c| c. is_whitespace ( ) ) , "http://" )
845
+ || has_prefix ( chars. skip_while ( |c| c. is_whitespace ( ) ) , "https://" )
846
+ }
847
+
848
+ /// Return the leading characters for an expression, if it's a string literal, f-string, or
849
+ /// string concatenation.
850
+ fn leading_chars ( expr : & Expr ) -> Option < impl Iterator < Item = char > + Clone + ' _ > {
851
+ match expr {
852
+ // Ex) `"foo"`
853
+ Expr :: StringLiteral ( ast:: ExprStringLiteral { value, .. } ) => {
854
+ Some ( Either :: Left ( value. chars ( ) ) )
855
+ }
856
+ // Ex) f"foo"
857
+ Expr :: FString ( ast:: ExprFString { value, .. } ) => {
858
+ value. elements ( ) . next ( ) . and_then ( |element| {
859
+ if let ast:: FStringElement :: Literal ( ast:: FStringLiteralElement {
860
+ value, ..
861
+ } ) = element
862
+ {
863
+ Some ( Either :: Right ( value. chars ( ) ) )
864
+ } else {
865
+ None
866
+ }
867
+ } )
868
+ }
869
+ // Ex) "foo" + "bar"
870
+ Expr :: BinOp ( ast:: ExprBinOp {
871
+ op : Operator :: Add ,
872
+ left,
873
+ ..
874
+ } ) => leading_chars ( left) ,
875
+ _ => None ,
876
+ }
877
+ }
878
+
841
879
let Some ( diagnostic_kind) = checker. semantic ( ) . resolve_qualified_name ( call. func . as_ref ( ) ) . and_then ( |qualified_name| {
842
880
match qualified_name. segments ( ) {
843
881
// Pickle
@@ -864,25 +902,11 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
864
902
[ "django" , "utils" , "safestring" | "html" , "mark_safe" ] => Some ( SuspiciousMarkSafeUsage . into ( ) ) ,
865
903
// URLOpen (`Request`)
866
904
[ "urllib" , "request" , "Request" ] |
867
- [ "six" , "moves" , "urllib" , "request" , "Request" ] => {
868
- // If the `url` argument is a string literal or an f string, allow `http` and `https` schemes.
905
+ [ "six" , "moves" , "urllib" , "request" , "Request" ] => {
906
+ // If the `url` argument is a string literal or an f- string, allow `http` and `https` schemes.
869
907
if call. arguments . args . iter ( ) . all ( |arg| !arg. is_starred_expr ( ) ) && call. arguments . keywords . iter ( ) . all ( |keyword| keyword. arg . is_some ( ) ) {
870
- match call. arguments . find_argument ( "url" , 0 ) {
871
- // If the `url` argument is a string literal, allow `http` and `https` schemes.
872
- Some ( Expr :: StringLiteral ( ast:: ExprStringLiteral { value, .. } ) ) => {
873
- if has_prefix ( value. chars ( ) . skip_while ( |c| c. is_whitespace ( ) ) , "http://" ) || has_prefix ( value. chars ( ) . skip_while ( |c| c. is_whitespace ( ) ) , "https://" ) {
874
- return None ;
875
- }
876
- } ,
877
- // If the `url` argument is an f-string literal, allow `http` and `https` schemes.
878
- Some ( Expr :: FString ( ast:: ExprFString { value, .. } ) ) => {
879
- if let Some ( ast:: FStringElement :: Literal ( ast:: FStringLiteralElement { value, .. } ) ) = value. elements ( ) . next ( ) {
880
- if has_prefix ( value. chars ( ) . skip_while ( |c| c. is_whitespace ( ) ) , "http://" ) || has_prefix ( value. chars ( ) . skip_while ( |c| c. is_whitespace ( ) ) , "https://" ) {
881
- return None ;
882
- }
883
- }
884
- } ,
885
- _ => { }
908
+ if call. arguments . find_argument ( "url" , 0 ) . and_then ( leading_chars) . is_some_and ( has_http_prefix) {
909
+ return None ;
886
910
}
887
911
}
888
912
Some ( SuspiciousURLOpenUsage . into ( ) )
@@ -892,43 +916,19 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
892
916
[ "six" , "moves" , "urllib" , "request" , "urlopen" | "urlretrieve" ] => {
893
917
if call. arguments . args . iter ( ) . all ( |arg| !arg. is_starred_expr ( ) ) && call. arguments . keywords . iter ( ) . all ( |keyword| keyword. arg . is_some ( ) ) {
894
918
match call. arguments . find_argument ( "url" , 0 ) {
895
- // If the `url` argument is a string literal, allow `http` and `https` schemes.
896
- Some ( Expr :: StringLiteral ( ast:: ExprStringLiteral { value, .. } ) ) => {
897
- if has_prefix ( value. chars ( ) . skip_while ( |c| c. is_whitespace ( ) ) , "http://" ) || has_prefix ( value. chars ( ) . skip_while ( |c| c. is_whitespace ( ) ) , "https://" ) {
898
- return None ;
899
- }
900
- } ,
901
-
902
- // If the `url` argument is an f-string literal, allow `http` and `https` schemes.
903
- Some ( Expr :: FString ( ast:: ExprFString { value, .. } ) ) => {
904
- if let Some ( ast:: FStringElement :: Literal ( ast:: FStringLiteralElement { value, .. } ) ) = value. elements ( ) . next ( ) {
905
- if has_prefix ( value. chars ( ) . skip_while ( |c| c. is_whitespace ( ) ) , "http://" ) || has_prefix ( value. chars ( ) . skip_while ( |c| c. is_whitespace ( ) ) , "https://" ) {
919
+ // If the `url` argument is a `urllib.request.Request` object, allow `http` and `https` schemes.
920
+ Some ( Expr :: Call ( ExprCall { func, arguments, .. } ) ) => {
921
+ if checker. semantic ( ) . resolve_qualified_name ( func. as_ref ( ) ) . is_some_and ( |name| name. segments ( ) == [ "urllib" , "request" , "Request" ] ) {
922
+ if arguments. find_argument ( "url" , 0 ) . and_then ( leading_chars) . is_some_and ( has_http_prefix) {
906
923
return None ;
907
924
}
908
925
}
909
926
} ,
910
927
911
- // If the `url` argument is a `urllib.request.Request` object, allow `http` and `https` schemes.
912
- Some ( Expr :: Call ( ExprCall { func, arguments, .. } ) ) => {
913
- if checker. semantic ( ) . resolve_qualified_name ( func. as_ref ( ) ) . is_some_and ( |name| name. segments ( ) == [ "urllib" , "request" , "Request" ] ) {
914
- match arguments. find_argument ( "url" , 0 ) {
915
- // If the `url` argument is a string literal, allow `http` and `https` schemes.
916
- Some ( Expr :: StringLiteral ( ast:: ExprStringLiteral { value, .. } ) ) => {
917
- if has_prefix ( value. chars ( ) . skip_while ( |c| c. is_whitespace ( ) ) , "http://" ) || has_prefix ( value. chars ( ) . skip_while ( |c| c. is_whitespace ( ) ) , "https://" ) {
918
- return None ;
919
- }
920
- } ,
921
-
922
- // If the `url` argument is an f-string literal, allow `http` and `https` schemes.
923
- Some ( Expr :: FString ( ast:: ExprFString { value, .. } ) ) => {
924
- if let Some ( ast:: FStringElement :: Literal ( ast:: FStringLiteralElement { value, .. } ) ) = value. elements ( ) . next ( ) {
925
- if has_prefix ( value. chars ( ) . skip_while ( |c| c. is_whitespace ( ) ) , "http://" ) || has_prefix ( value. chars ( ) . skip_while ( |c| c. is_whitespace ( ) ) , "https://" ) {
926
- return None ;
927
- }
928
- }
929
- } ,
930
- _ => { }
931
- }
928
+ // If the `url` argument is a string literal, allow `http` and `https` schemes.
929
+ Some ( expr) => {
930
+ if leading_chars ( expr) . is_some_and ( has_http_prefix) {
931
+ return None ;
932
932
}
933
933
} ,
934
934
0 commit comments