[Python] CWE-348: Client supplied ip used in security check#6214
Conversation
yoff
left a comment
There was a problem hiding this comment.
Generally clean and well structured 👍. There are a number of uncommented source and sink implementations.
| TornadoClientSuppliedIpUsedInSecurityCheck() { | ||
| exists(RemoteFlowSource rfs, DataFlow::LocalSourceNode lsn | | ||
| rfs.getSourceType() = "tornado.web.RequestHandler" and rfs.asCfgNode() = lsn.asCfgNode() | ||
| | | ||
| lsn.flowsTo(DataFlow::exprNode(this.getFunction() | ||
| .asExpr() | ||
| .(Attribute) | ||
| .getObject() | ||
| .(Attribute) | ||
| .getObject() | ||
| .(Attribute) | ||
| .getObject())) and | ||
| this.getFunction().asExpr().(Attribute).getName() in ["get", "get_list"] and | ||
| this.getFunction().asExpr().(Attribute).getObject().(Attribute).getName() = "headers" and | ||
| this.getArg(0).asCfgNode().getNode().(StrConst).getText().toLowerCase() = | ||
| clientIpParameterName() | ||
| ) | ||
| } |
There was a problem hiding this comment.
I suggest not quantifying over the local source node, since it will just be rfs. Instead I suggest adding a shorthand for this.getFunction().(Attribute). And then to use DataFlow::AttrRead instead of Attribute as it operates on a higher level. Finally, I would comment the long chain of casts.
| TornadoClientSuppliedIpUsedInSecurityCheck() { | |
| exists(RemoteFlowSource rfs, DataFlow::LocalSourceNode lsn | | |
| rfs.getSourceType() = "tornado.web.RequestHandler" and rfs.asCfgNode() = lsn.asCfgNode() | |
| | | |
| lsn.flowsTo(DataFlow::exprNode(this.getFunction() | |
| .asExpr() | |
| .(Attribute) | |
| .getObject() | |
| .(Attribute) | |
| .getObject() | |
| .(Attribute) | |
| .getObject())) and | |
| this.getFunction().asExpr().(Attribute).getName() in ["get", "get_list"] and | |
| this.getFunction().asExpr().(Attribute).getObject().(Attribute).getName() = "headers" and | |
| this.getArg(0).asCfgNode().getNode().(StrConst).getText().toLowerCase() = | |
| clientIpParameterName() | |
| ) | |
| } | |
| TornadoClientSuppliedIpUsedInSecurityCheck() { | |
| exists(RemoteFlowSource rfs, DataFlow::AttrRead get | | |
| rfs.getSourceType() = "tornado.web.RequestHandler" and | |
| this.getFunction() = get | |
| | | |
| // `get` is a call to `rfs`.request.headers.get | |
| // `rfs`.request.headers | |
| get.getObject() | |
| .(DataFlow::AttrRead) | |
| // `rfs`.request | |
| .getObject() | |
| .(DataFlow::AttrRead) | |
| // `rfs` | |
| .getObject() | |
| .getALocalSource() = rfs and | |
| get.getAttributeName() in ["get", "get_list"] and | |
| get.getObject().(DataFlow::AttrRead).getAttributeName() = "headers" and | |
| this.getArg(0).asExpr().(StrConst).getText().toLowerCase() = clientIpParameterName() | |
| ) | |
| } |
A similar rewrite could be made to the one for Flask.
| * | ||
| * For example: `request.headers.get("X-Forwarded-For")`. | ||
| */ | ||
| abstract class ClientSuppliedIpUsedInSecurityCheck extends DataFlow::CallCfgNode { } |
There was a problem hiding this comment.
I would rename this to ClientSuppliedIp (and similarly drop the suffix on all the implementations) since that is what we know about these nodes. It also reads better in the configuration: "A client supplied IP (source) flows to a security check (sink)".
| } | ||
|
|
||
| /** A data flow sink for ip address forgery vulnerabilities. */ | ||
| abstract class ClientSuppliedIpUsedInSecurityCheckSink extends DataFlow::Node { } |
There was a problem hiding this comment.
I would rename this class to PossibleSecurityCheck or SecurityCheck.
| abstract class ClientSuppliedIpUsedInSecurityCheckSink extends DataFlow::Node { } | ||
|
|
||
| /** A data flow sink for sql operation. */ | ||
| private class SqlOperationSink extends ClientSuppliedIpUsedInSecurityCheckSink { |
There was a problem hiding this comment.
I would rename this class to SqlOperationAsSecurityCheck
| override predicate isSanitizer(DataFlow::Node node) { | ||
| exists(Subscript ss | | ||
| not ss.getIndex().(IntegerLiteral).getText() = "0" and | ||
| ss.getObject().(Call).getFunc().(Attribute).getName() = "split" and | ||
| ss.getObject().(Call).getArg(0).(StrConst).getText() = "," and | ||
| ss.getObject().(Call).getFunc().(Attribute).getObject() = node.asExpr() | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Should the sanitizer (node) not be the result of the subscripting rather than object? Consider the code
client_ip = request.headers.get('x-forwarded-for')
dummy = client_ip.split(",")[2]
if not client_ip == '127.0.0.1':
raise Exception('ip illegal')This would be deemed sanitized as the flow is blocked in the line assigning dummy. Yet, no change was made to client_ip prior to testing it. (If the sanitizer is the subscript, the flow is blocked into dummy, but there is still use-use-flow to the variable on the next line.)
There was a problem hiding this comment.
Is there such a way of writing in real open source projects?
There was a problem hiding this comment.
There was a problem hiding this comment.
X-Forwarded-For is a superposition process when used, format: client, proxy1, proxy2. X-Real-IP is a process of coverage when in use.
There was a problem hiding this comment.
It is possible that we have found a possible improvement to the java query :-) While this particular pattern looks unlikely, I think it could easily happen as a typo, at which point an alert would be great. And other patterns could be constructed as well, so we should always strive to point to the correct node. In any case, it should not be hard to fix.
There was a problem hiding this comment.
OK. Can you elaborate on this disinfectant? Let me optimize the configuration.
There was a problem hiding this comment.
override predicate isSanitizer(DataFlow::Node node) {
exists(Subscript ss |
ss.getObject().(Call).getFunc().(Attribute).getName() = "split" and
ss.getObject().(Call).getArg(0).(StrConst).getText() = "," and
ss = node.asExpr()
)
}@yoff Do you want this result?
There was a problem hiding this comment.
Yes exactly. Except I think you still need to exclude index 0.
|
Friendly ping. |
Thanks for the heads up, I must have missed the notification. I did not mean for 10 days to pass by! |
Please review, thank you! |
yoff
left a comment
There was a problem hiding this comment.
I believe you still need to inspect the index? Otherwise looks fine now.
…edInSecurityCheck.ql Co-authored-by: yoff <lerchedahl@gmail.com>
|
The language tests fail because we now have a mechanism for computing subpaths. You can merge from main and rerun tests. I will also make a suggestion, that I think will make them pass.. |
…tSuppliedIpUsedInSecurityCheck.expected Co-authored-by: yoff <lerchedahl@gmail.com>
Do I need to make other changes? |
No, I think it is fine for now :-) |
If an application trusts an HTTP request header like X-Forwarded-For to accurately specify the remote IP address of the connecting client.