-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Python: Add cookie security-related queries #6360
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: Add cookie security-related queries #6360
Conversation
… into jorgectf/python/insecure-cookie
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have not looked at this PR in depth, but just noted that this deals with setting cookies in HTTP responses. Would be great if you could integrate this with the existing HTTP::Server::CookieWrite
concept implemented here:
codeql/python/ql/lib/semmle/python/Concepts.qll
Lines 759 to 784 in d97b130
/** | |
* A data-flow node that sets a cookie in an HTTP response. | |
* | |
* Extend this class to refine existing API models. If you want to model new APIs, | |
* extend `HTTP::CookieWrite::Range` instead. | |
*/ | |
class CookieWrite extends DataFlow::Node { | |
CookieWrite::Range range; | |
CookieWrite() { this = range } | |
/** | |
* Gets the argument, if any, specifying the raw cookie header. | |
*/ | |
DataFlow::Node getHeaderArg() { result = range.getHeaderArg() } | |
/** | |
* Gets the argument, if any, specifying the cookie name. | |
*/ | |
DataFlow::Node getNameArg() { result = range.getNameArg() } | |
/** | |
* Gets the argument, if any, specifying the cookie value. | |
*/ | |
DataFlow::Node getValueArg() { result = range.getValueArg() } | |
} |
You can do this by creating your own experimental concepts that extend the existing concept, for an exmaple, see
codeql/python/ql/lib/semmle/python/Concepts.qll
Lines 75 to 106 in 85f00fd
/** | |
* A data flow node that writes data to the file system access. | |
* | |
* Extend this class to refine existing API models. If you want to model new APIs, | |
* extend `FileSystemWriteAccess::Range` instead. | |
*/ | |
class FileSystemWriteAccess extends FileSystemAccess { | |
override FileSystemWriteAccess::Range range; | |
/** | |
* Gets a node that represents data to be written to the file system (possibly with | |
* some transformation happening before it is written, like JSON encoding). | |
*/ | |
DataFlow::Node getADataNode() { result = range.getADataNode() } | |
} | |
/** Provides a class for modeling new file system writes. */ | |
module FileSystemWriteAccess { | |
/** | |
* A data flow node that writes data to the file system access. | |
* | |
* Extend this class to model new APIs. If you want to refine existing API models, | |
* extend `FileSystemWriteAccess` instead. | |
*/ | |
abstract class Range extends FileSystemAccess::Range { | |
/** | |
* Gets a node that represents data to be written to the file system (possibly with | |
* some transformation happening before it is written, like JSON encoding). | |
*/ | |
abstract DataFlow::Node getADataNode(); | |
} | |
} |
I would base my modeling in Django/Flask on the existing modeling (that is, copy-paste it), such that it is easy to just override the existing modeling when promoting this query 👍
I haven't been able to find any CVE for this specific security issue, so the bounty submission has been closed. |
Thanks for the update @jorgectf. Will take a look on this PR soon 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found 5 vulnerabilities.
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) { | ||
t.start() and | ||
result instanceof InstanceSource | ||
or | ||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t)) | ||
} |
Check warning
Code scanning / CodeQL
Dead code
} | ||
|
||
/** Gets a reference to an instance of `django.http.response.HttpResponse`. */ | ||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) } |
Check warning
Code scanning / CodeQL
Dead code
/** | ||
* Gets a header setting a cookie. | ||
* | ||
* Given the following example: | ||
* | ||
* ```py | ||
* @app.route("/") | ||
* def flask_make_response(): | ||
* resp = make_response("") | ||
* resp.headers['Set-Cookie'] = "name=value; Secure;" | ||
* return resp | ||
* ``` | ||
* | ||
* * `this` would be `resp.headers['Set-Cookie'] = "name=value; Secure;"`. | ||
* * `isSecure()` predicate would succeed. | ||
* * `isHttpOnly()` predicate would fail. | ||
* * `isSameSite()` predicate would fail. | ||
* * `getName()` and `getValue()` results would be `"name=value; Secure;"`. | ||
*/ |
Check warning
Code scanning / CodeQL
Class QLDoc style.
/** | ||
* Gets a call to `set_cookie()`. | ||
* | ||
* Given the following example: | ||
* | ||
* ```py | ||
* @app.route("/") | ||
* def false(): | ||
* resp = make_response() | ||
* resp.set_cookie("name", value="value", secure=True, httponly=True, samesite='Lax') | ||
* return resp | ||
* ``` | ||
* | ||
* * `this` would be `resp.set_cookie("name", value="value", secure=False, httponly=False, samesite='None')`. | ||
* * `getName()`'s result would be `"name"`. | ||
* * `getValue()`'s result would be `"value"`. | ||
* * `isSecure()` predicate would succeed. | ||
* * `isHttpOnly()` predicate would succeed. | ||
* * `isSameSite()` predicate would succeed. | ||
*/ |
Check warning
Code scanning / CodeQL
Class QLDoc style.
/** | ||
* Gets a call to `set_cookie()`. | ||
* | ||
* Given the following example: | ||
* | ||
* ```py | ||
* def django_response(request): | ||
* resp = django.http.HttpResponse() | ||
* resp.set_cookie("name", "value", secure=True, httponly=True, samesite='Lax') | ||
* return resp | ||
* ``` | ||
* | ||
* * `this` would be `resp.set_cookie("name", "value", secure=False, httponly=False, samesite='None')`. | ||
* * `getName()`'s result would be `"name"`. | ||
* * `getValue()`'s result would be `"value"`. | ||
* * `isSecure()` predicate would succeed. | ||
* * `isHttpOnly()` predicate would succeed. | ||
* * `isSameSite()` predicate would succeed. | ||
*/ |
Check warning
Code scanning / CodeQL
Class QLDoc style.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took forever to review this, sorry 😬
For reference when promoting this, JS adopted an approach with multiple queries for each of these flags, so we should consider doing the same
js/samesite-none-cookie
(precision medium)js/client-exposed-cookie
(precision high) -- forhttpOnly
flagjs/clear-text-cookie
(precision high) -- for 'secure' flag
I also think we need to review how bad CookieInjection really is. My thoughts without reading up on this at all is that it should be a @precision medium
query -- a user can always set cookies on their own machine, so this is only a problem when a third-party attacker can set a cookie on a victim machine, meaning some cookie writing request without CSRF protection... but my intuition might be wrong 🤷
62b3ec1
to
fc8633c
Compare
This PR adds:
Modeling of:
flask
.django
.Queries:
(secure|httponly|samesite)
flag properly set.