@@ -83,29 +83,85 @@ module ServerSideRequestForgery {
83
83
/**
84
84
* A string construction (concat, format, f-string) where the left side is not
85
85
* user-controlled.
86
+ *
87
+ * For all of these cases, we try to allow `http://` or `https://` on the left side
88
+ * since that will still allow full URL control.
86
89
*/
87
90
class StringConstructioneAsFullUrlControlSanitizer extends FullUrlControlSanitizer {
88
91
StringConstructioneAsFullUrlControlSanitizer ( ) {
89
92
// string concat
90
93
exists ( BinaryExprNode add |
91
94
add .getOp ( ) instanceof Add and
92
- add .getRight ( ) = this .asCfgNode ( )
95
+ add .getRight ( ) = this .asCfgNode ( ) and
96
+ not add .getLeft ( ) .getNode ( ) .( StrConst ) .getText ( ) .toLowerCase ( ) in [ "http://" , "https://" ]
93
97
)
94
98
or
95
99
// % formatting
96
100
exists ( BinaryExprNode fmt |
97
101
fmt .getOp ( ) instanceof Mod and
98
- fmt .getRight ( ) = this .asCfgNode ( )
102
+ fmt .getRight ( ) = this .asCfgNode ( ) and
103
+ // detecting %-formatting is not super easy, so we simplify it to only handle
104
+ // when there is a **single** substitution going on.
105
+ not fmt .getLeft ( ) .getNode ( ) .( StrConst ) .getText ( ) .regexpMatch ( "^(?i)https?://%s[^%]*$" )
99
106
)
100
107
or
101
108
// arguments to a format call
102
- exists ( DataFlow:: MethodCallNode call |
109
+ exists ( DataFlow:: MethodCallNode call , string httpPrefixRe |
110
+ httpPrefixRe = "^(?i)https?://(?:(\\{\\})|\\{([0-9]+)\\}|\\{([^0-9].*)\\}).*$"
111
+ |
103
112
call .getMethodName ( ) = "format" and
104
- this in [ call .getArg ( _) , call .getArgByName ( _) ]
113
+ (
114
+ if call .getObject ( ) .asExpr ( ) .( StrConst ) .getText ( ) .regexpMatch ( httpPrefixRe )
115
+ then
116
+ exists ( string text | text = call .getObject ( ) .asExpr ( ) .( StrConst ) .getText ( ) |
117
+ // `http://{}...`
118
+ exists ( text .regexpCapture ( httpPrefixRe , 1 ) ) and
119
+ this in [ call .getArg ( any ( int i | i >= 1 ) ) , call .getArgByName ( _) ]
120
+ or
121
+ // `http://{123}...`
122
+ exists ( int safeArgIndex | safeArgIndex = text .regexpCapture ( httpPrefixRe , 2 ) .toInt ( ) |
123
+ this in [ call .getArg ( any ( int i | i != safeArgIndex ) ) , call .getArgByName ( _) ]
124
+ )
125
+ or
126
+ // `http://{abc}...`
127
+ exists ( string safeArgName | safeArgName = text .regexpCapture ( httpPrefixRe , 3 ) |
128
+ this in [ call .getArg ( _) , call .getArgByName ( any ( string s | s != safeArgName ) ) ]
129
+ )
130
+ )
131
+ else this in [ call .getArg ( _) , call .getArgByName ( _) ]
132
+ )
105
133
)
106
134
or
107
135
// f-string
108
- exists ( Fstring fstring | fstring .getValue ( any ( int i | i > 0 ) ) = this .asExpr ( ) )
136
+ exists ( Fstring fstring |
137
+ if fstring .getValue ( 0 ) .( StrConst ) .getText ( ) .toLowerCase ( ) in [ "http://" , "https://" ]
138
+ then fstring .getValue ( any ( int i | i >= 2 ) ) = this .asExpr ( )
139
+ else fstring .getValue ( any ( int i | i >= 1 ) ) = this .asExpr ( )
140
+ )
109
141
}
110
142
}
111
143
}
144
+
145
+ predicate debug ( Location loc , DataFlow:: MethodCallNode call , string text , DataFlow:: Node safe ) {
146
+ loc = call .getLocation ( ) and
147
+ call .getMethodName ( ) = "format" and
148
+ text = call .getObject ( ) .asExpr ( ) .( StrConst ) .getText ( ) and
149
+ exists ( string httpPrefixRe |
150
+ httpPrefixRe = "^(?i)https?://(?:(\\{\\})|\\{([0-9]+)\\}|\\{([^0-9].*)\\}).*$" and
151
+ text .regexpMatch ( httpPrefixRe )
152
+ |
153
+ // `http://{123}...`
154
+ exists ( int safeArgIndex | safeArgIndex = text .regexpCapture ( httpPrefixRe , 2 ) .toInt ( ) |
155
+ safe = call .getArg ( safeArgIndex )
156
+ )
157
+ or
158
+ // `http://{abc}...`
159
+ exists ( string safeArgName | safeArgName = text .regexpCapture ( httpPrefixRe , 3 ) |
160
+ safe = call .getArgByName ( safeArgName )
161
+ )
162
+ or
163
+ // `http://{}...`
164
+ exists ( text .regexpCapture ( httpPrefixRe , 1 ) ) and
165
+ safe = call .getArg ( 0 )
166
+ )
167
+ }
0 commit comments