Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Python] : Cors misconfiguration #16989

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions python/ql/src/experimental/Security/CWE-346/CorsMisconfig.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
<qhelp>
<overview>
<p>
This configuration can give the attacker the ability to control <code>Access-Control-Allow-Origin</code> , and also allow the inclusion of cookies on the cross-origin request,
(i.e. when the <code>Access-Control-Allow-Credentials</code> header is set to true)
meaning that Peer B can send a request to Peer A that will include the cookies as if the request was executed by the user.
</p>
</overview>
<recommendation>
<p>
When configuring CORS that allow credentials passing,
it's best not to use user-provided values for the allowed origins response header,
especially if the cookies grant session permissions on the user's account.
</p>
</recommendation>
<example>
<p>
the example below shows the study case. The request header
<code>origins</code> controls the allowed origins for such a
Cross-Origin request.
</p>

<sample src="test.py" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example file appears to be missing.
(if the test.py file from the test directory is intended to be used as the example here, it should be copied into this directory).

Additionally, the closing </example> tag appears to be missing.

<references>
<li>Mozilla Developer Network: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin">CORS, Access-Control-Allow-Origin</a>.</li>
<li>Mozilla Developer Network: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials">CORS, Access-Control-Allow-Credentials</a>.</li>
<li>PortSwigger: <a href="http://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html">Exploiting CORS Misconfigurations for Bitcoins and Bounties</a></li>
<li>W3C: <a href="https://w3c.github.io/webappsec-cors-for-developers/#resources">CORS for developers, Advice for Resource Owners</a></li>
</references>
</qhelp>
70 changes: 70 additions & 0 deletions python/ql/src/experimental/Security/CWE-346/CorsMisconfig.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @name Cors misconfiguration
* @description CORS header is delivered by untrusted input, allowing a remote user to control which origins are trusted.
* @kind path-problem
* @problem.severity error
* @precision high
* @id py/unvalidated-cors-origin-set
* @tags security
* external/cwe/cwe-346
*/

import python
import experimental.semmle.python.Concepts
import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.RemoteFlowSources
import semmle.python.dataflow.new.TaintTracking2

/**
* Provides the name of the `Access-Control-Allow-Origin` header key.
*/
string headerAllowOrigin() { result = "Access-Control-Allow-Origin".toLowerCase() }

/**
* Holds if `header` sets `Access-Control-Allow-Credentials` to `true`.
*/
private predicate setsAllowCredentials(HeaderDeclaration header) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HeaderDeclaration concept has recently been removed from the experimental library, with its functionality and modelling being provided by the Http::Server::ResponseHeaderWrite concept from the main library (semmle.python.Concepts) instead. Uses here will need to be replaced with that concept in order for this query to compile on main.

header.getNameArg().asExpr().(StrConst).getText().toLowerCase() =
"access-control-allow-credentials" and
header.getValueArg().asExpr().(StrConst).getText().toLowerCase() = "true"
}

/**
* This taintflow2 configuration checks if there is a flow from source node towards CorsProbableCheckAccess methods.
*/
class CorsSourceReachesCheckConfig extends TaintTracking2::Configuration {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be a more complex (and I'd say optional) change, but this second configuration could be expressed as a BarrierGaurd sanitizer instead.

CorsSourceReachesCheckConfig() { this = "CorsOriginConfig" }

override predicate isSource(DataFlow::Node source) { any(CorsOriginConfig c).hasFlow(source, _) }

override predicate isSink(DataFlow::Node sink) {
exists(Compare cmp, Expr left, Expr right, Cmpop cmpop |
cmpop.getSymbol() = ["==", "in", "is not", "!="] and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is is also intended to be a possible comparison operator here?
Or if in is intended to be checked, should not in also be checked?

cmp.compares(left, cmpop, right) and
sink.asExpr() = [left, right]
)
}
}

private class CorsOriginConfig extends TaintTracking::Configuration {
CorsOriginConfig() { this = "CorsOriginConfig" }

override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }

override predicate isSink(DataFlow::Node sink) {
exists(HeaderDeclaration corsHeader, HeaderDeclaration allowCredentialsHeader |
headerAllowOrigin() = corsHeader.getNameArg().asExpr().(StrConst).getText().toLowerCase() and
setsAllowCredentials(allowCredentialsHeader) and
corsHeader.getEnclosingCallable() = allowCredentialsHeader.getEnclosingCallable() and
sink = corsHeader.getValueArg()
)
}
}

from
DataFlow::PathNode source, DataFlow::PathNode sink, CorsOriginConfig conf,
CorsSourceReachesCheckConfig sanconf
where conf.hasFlowPath(source, sink) and not sanconf.hasFlow(source.getNode(), _)
select sink.getNode(), source, sink, "CORS header is being set using user controlled value $@.",
source.getNode(), "user-provided value"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| test.py:7:55:7:64 | ControlFlowNode for rfs_header | test.py:5:18:5:24 | ControlFlowNode for request | test.py:7:55:7:64 | ControlFlowNode for rfs_header | CORS header is being set using user controlled value $@. | test.py:5:18:5:24 | ControlFlowNode for request | user-provided value |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the test.qlref file missing here?

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from flask import Response, request, Flask

@app.route("/flask_Response")
def flask_Response():
rfs_header = request.args["rfs_header"]
response = Response()
response.headers['Access-Control-Allow-Origin'] = rfs_header
response.headers['Access-Control-Allow-Credentials'] = "true"
return response
Loading