Skip to content

Commit 8bf8893

Browse files
committed
Add support for vulnerable CORS middlewares
1 parent e81fdc9 commit 8bf8893

File tree

11 files changed

+337
-0
lines changed

11 files changed

+337
-0
lines changed

python/ql/lib/semmle/python/Concepts.qll

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,6 +1411,56 @@ module Http {
14111411
override DataFlow::Node getValueArg() { none() }
14121412
}
14131413

1414+
/**
1415+
* A data-flow node that enables or disables CORS
1416+
* in a global manner.
1417+
*
1418+
* Extend this class to refine existing API models. If you want to model new APIs,
1419+
* extend `CorsMiddleware::Range` instead.
1420+
*/
1421+
class CorsMiddleware extends DataFlow::Node instanceof CorsMiddleware::Range {
1422+
/**
1423+
* Gets the string corresponding to the middleware
1424+
*/
1425+
string middleware_name() { result = super.middleware_name() }
1426+
1427+
/**
1428+
* Gets the boolean value corresponding to if CORS credentials is enabled
1429+
* (`true`) or disabled (`false`) by this node.
1430+
*/
1431+
DataFlow::Node allowed_origins() { result = super.allowed_origins() }
1432+
1433+
DataFlow::Node allowed_credentials() { result = super.allowed_credentials() }
1434+
}
1435+
1436+
/** Provides a class for modeling new CORS middleware APIs. */
1437+
module CorsMiddleware {
1438+
/**
1439+
* A data-flow node that enables or disables Cross-site request forgery protection
1440+
* in a global manner.
1441+
*
1442+
* Extend this class to model new APIs. If you want to refine existing API models,
1443+
* extend `CorsMiddleware` instead.
1444+
*/
1445+
abstract class Range extends DataFlow::Node {
1446+
/**
1447+
* Gets the string corresponding to the middleware
1448+
*/
1449+
abstract string middleware_name();
1450+
1451+
/**
1452+
* Gets the boolean value corresponding to if CORS credentials is enabled
1453+
* (`true`) or disabled (`false`) by this node.
1454+
*/
1455+
abstract DataFlow::Node allowed_credentials();
1456+
1457+
/**
1458+
* Gets the strings corresponding to the origins allowed by the cors policy
1459+
*/
1460+
abstract DataFlow::Node allowed_origins();
1461+
}
1462+
}
1463+
14141464
/**
14151465
* A data-flow node that enables or disables Cross-site request forgery protection
14161466
* in a global manner.

python/ql/lib/semmle/python/frameworks/FastApi.qll

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,32 @@ module FastApi {
3030
API::Node instance() { result = cls().getReturn() }
3131
}
3232

33+
/**
34+
* A call to `app.add_middleware` adding a generic middleware.
35+
*/
36+
private class AddMiddlewareCall extends DataFlow::CallCfgNode {
37+
AddMiddlewareCall() { this = App::instance().getMember("add_middleware").getACall() }
38+
39+
string middleware_name() { result = this.getArg(0).asExpr().(Name).toString() }
40+
}
41+
42+
/**
43+
* A call to `app.add_middleware` adding CORSMiddleware.
44+
*/
45+
class AddCorsMiddlewareCall extends Http::Server::CorsMiddleware::Range, AddMiddlewareCall {
46+
override string middleware_name() { result = this.getArg(0).asExpr().(Name).toString() }
47+
48+
override DataFlow::Node allowed_origins() { result = this.getArgByName("allow_origins") }
49+
50+
override DataFlow::Node allowed_credentials() {
51+
result = this.getArgByName("allow_credentials")
52+
}
53+
54+
DataFlow::Node allowed_methods() { result = this.getArgByName("allow_methods") }
55+
56+
DataFlow::Node allowed_headers() { result = this.getArgByName("allow_headers") }
57+
}
58+
3359
/**
3460
* Provides models for the `fastapi.APIRouter` class
3561
*

python/ql/lib/semmle/python/frameworks/Starlette.qll

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,87 @@ private import semmle.python.frameworks.data.ModelsAsData
2525
* - https://www.starlette.io/
2626
*/
2727
module Starlette {
28+
/**
29+
* Provides models for the `starlette.app` class
30+
*
31+
* See https://www.starlette.io/websockets/.
32+
*/
33+
module App {
34+
API::Node cls() { result = API::moduleImport("starlette").getMember("app") }
35+
36+
/** Gets a reference to a FastAPI application (an instance of `fastapi.FastAPI`). */
37+
API::Node instance() { result = cls().getReturn() }
38+
}
39+
40+
/**
41+
* A call to any of the execute methods on a `app.add_middleware`.
42+
*/
43+
class AddMiddlewareCall extends DataFlow::CallCfgNode {
44+
AddMiddlewareCall() {
45+
this = [App::instance().getMember("add_middleware").getACall(), Middleware::instance()]
46+
}
47+
48+
string middleware_name() { result = this.getArg(0).asExpr().(Name).toString() }
49+
}
50+
51+
/**
52+
* A call to any of the execute methods on a `app.add_middleware` with CORSMiddleware.
53+
*/
54+
class AddCorsMiddlewareCall extends AddMiddlewareCall, Http::Server::CorsMiddleware::Range {
55+
override string middleware_name() { result = this.getArg(0).asExpr().(Name).toString() }
56+
57+
override DataFlow::Node allowed_origins() { result = this.getArgByName("allow_origins") }
58+
59+
override DataFlow::Node allowed_credentials() {
60+
result = this.getArgByName("allow_credentials")
61+
}
62+
63+
DataFlow::Node allowed_methods() { result = this.getArgByName("allow_methods") }
64+
65+
DataFlow::Node allowed_headers() { result = this.getArgByName("allow_headers") }
66+
}
67+
68+
/**
69+
* Provides models for the `starlette.middleware.Middleware` class
70+
*
71+
* See https://www.starlette.io/.
72+
*/
73+
module Middleware {
74+
/** Gets a reference to the `starlette.middleware.Middleware` class. */
75+
API::Node classRef() {
76+
result = API::moduleImport("starlette").getMember("middleware").getMember("Middleware")
77+
or
78+
result = ModelOutput::getATypeNode("starlette.middleware.Middleware~Subclass").getASubclass*()
79+
}
80+
81+
/**
82+
* A source of instances of `starlette.middleware.Middleware`, extend this class to model new instances.
83+
*
84+
* This can include instantiations of the class, return values from function
85+
* calls, or a special parameter that will be set when functions are called by an external
86+
* library.
87+
*
88+
* Use the predicate `Middleware::instance()` to get references to instances of `starlette.middleware.middleware`.
89+
*/
90+
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
91+
92+
/** A direct instantiation of `starlette.middleware.Middleware`. */
93+
class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
94+
ClassInstantiation() { this = classRef().getACall() }
95+
}
96+
97+
/** Gets a reference to an instance of `starlette.middleware.Middleware`. */
98+
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
99+
t.start() and
100+
result instanceof InstanceSource
101+
or
102+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
103+
}
104+
105+
/** Gets a reference to an instance of `starlette.middleware.Middleware`. */
106+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
107+
}
108+
28109
/**
29110
* Provides models for the `starlette.websockets.WebSocket` class
30111
*
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
Web browsers, by default, disallow cross-origin resource sharing via direct HTTP requests (i.e. using a JavaScript HTTP client).
8+
Still, to satisfy some needs that arose with the growth of the web, an expedient was created to make exceptions possible.
9+
CORS (Cross-origin resource sharing) is a mechanism that allows resources of a web endpoint (let's call it "Peer A")
10+
to be accessed from another web page belonging to a different domain ("Peer B").
11+
</p>
12+
<p>
13+
For that to happen, Peer A needs to make available its CORS configuration via special headers on the desired endpoint
14+
via the OPTIONS method.
15+
</p>
16+
<p>
17+
This configuration can also allow the inclusion of cookies on the cross-origin request,
18+
(i.e. when the <code>Access-Control-Allow-Credentials</code> header is set to true)
19+
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.
20+
</p>
21+
<p>
22+
That can have dangerous effects if Peer B origin is not restricted correctly.
23+
An example of a dangerous scenario is when <code>Access-Control-Allow-Origin</code> header is set to a value gotten from the Peer B's request
24+
(and not correctly validated), or is set to special values such as <code>*</code> or <code>null</code>.
25+
The above values can allow any Peer B to send requests to the misconfigured Peer A on behalf of the user.
26+
</p>
27+
<p>
28+
Example scenario:
29+
User is client of a bank that has its API misconfigured to accept CORS requests from any domain.
30+
When the user loads an evil page, the evil page sends a request to the bank's API to transfer all funds
31+
to evil party's account.
32+
Given that the user was already logged in to the bank website, and had its session cookies set,
33+
the evil party's request succeeds.
34+
</p>
35+
</overview>
36+
<recommendation>
37+
<p>
38+
When configuring CORS that allow credentials passing,
39+
it's best not to use user-provided values for the allowed origins response header,
40+
especially if the cookies grant session permissions on the user's account.
41+
</p>
42+
<p>
43+
It also can be very dangerous to set the allowed origins to <code>null</code> (which can be bypassed).
44+
</p>
45+
</recommendation>
46+
<example>
47+
<p>
48+
The first example shows a few possible CORS misconfiguration cases:
49+
</p>
50+
<sample src="CorsMisconfigurationMiddlewareBad.py"/>
51+
<p>
52+
The second example show better configurations:
53+
</p>
54+
<sample src="CorsMisconfigurationMiddlewareGood.py"/>
55+
</example>
56+
<references>
57+
<li>
58+
Reference 1: <a href="https://portswigger.net/web-security/cors">PortSwigger Web Security Academy on CORS</a>.
59+
</li>
60+
<li>
61+
Reference 2: <a href="https://www.youtube.com/watch?v=wgkj4ZgxI4c">AppSec EU 2017 Exploiting CORS Misconfigurations For Bitcoins And Bounties by James Kettle</a>.
62+
</li>
63+
</references>
64+
</qhelp>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* @name SOP protection weak with credentials
3+
* @description Disabling or weakening SOP protection may make the application
4+
* vulnerable to a CORS attack.
5+
* @kind problem
6+
* @problem.severity warning
7+
* @security-severity 8.8
8+
* @precision high
9+
* @id py/insecure-cors-setting
10+
* @tags security
11+
* external/cwe/cwe-352
12+
*/
13+
14+
import python
15+
import semmle.python.Concepts
16+
private import semmle.python.dataflow.new.DataFlow
17+
predicate containsStar(DataFlow::Node array){
18+
(array.asExpr() instanceof List and
19+
array.asExpr().getASubExpression().(StringLiteral).getText().matches("*")) or
20+
(array.asExpr().(StringLiteral).getText().matches(["*", "null"]))
21+
22+
}
23+
24+
predicate isCorsMiddleware(Http::Server::CorsMiddleware middleware){
25+
middleware.middleware_name().matches("CORSMiddleware")
26+
}
27+
28+
predicate credentialsAllowed(Http::Server::CorsMiddleware middleware){
29+
middleware.allowed_credentials().asExpr() instanceof True
30+
}
31+
32+
from Http::Server::CorsMiddleware a
33+
where credentialsAllowed(a) and
34+
containsStar(a.allowed_origins().getALocalSource()) and
35+
isCorsMiddleware(a)
36+
select a, "This CORS middleware uses a vulnerable configuration that leaves it open to attacks from arbitrary websites"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from fastapi import FastAPI
2+
from fastapi.middleware.cors import CORSMiddleware
3+
4+
app = FastAPI()
5+
6+
origins = [
7+
"*"
8+
]
9+
10+
app.add_middleware(
11+
CORSMiddleware,
12+
allow_origins=origins,
13+
allow_credentials=True,
14+
allow_methods=["*"],
15+
allow_headers=["*"],
16+
)
17+
18+
19+
@app.get("/")
20+
async def main():
21+
return {"message": "Hello World"}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from fastapi import FastAPI
2+
from fastapi.middleware.cors import CORSMiddleware
3+
4+
app = FastAPI()
5+
6+
origins = [
7+
"http://localhost.tiangolo.com",
8+
"https://localhost.tiangolo.com",
9+
"http://localhost",
10+
"http://localhost:8080",
11+
]
12+
13+
app.add_middleware(
14+
CORSMiddleware,
15+
allow_origins=origins,
16+
allow_credentials=True,
17+
allow_methods=["*"],
18+
allow_headers=["*"],
19+
)
20+
21+
22+
@app.get("/")
23+
async def main():
24+
return {"message": "Hello World"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
| fastapi.py:10:1:16:1 | ControlFlowNode for Attribute() | This CORS middleware uses a vulnerable configuration that leaves it open to attacks from arbitrary websites |
2+
| starlette.py:8:5:8:75 | ControlFlowNode for Middleware() | This CORS middleware uses a vulnerable configuration that leaves it open to attacks from arbitrary websites |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE-942/CorsMisconfigurationMiddleware.ql
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from fastapi import FastAPI
2+
from fastapi.middleware.cors import CORSMiddleware
3+
4+
app = FastAPI()
5+
6+
origins = [
7+
"*"
8+
]
9+
10+
app.add_middleware(
11+
CORSMiddleware,
12+
allow_origins=origins,
13+
allow_credentials=True,
14+
allow_methods=["*"],
15+
allow_headers=["*"],
16+
)
17+
18+
19+
@app.get("/")
20+
async def main():
21+
return {"message": "Hello World"}

0 commit comments

Comments
 (0)