diff --git a/python/ql/lib/semmle/python/Concepts.qll b/python/ql/lib/semmle/python/Concepts.qll index 8e4f810d4a0..04d4d63aca3 100644 --- a/python/ql/lib/semmle/python/Concepts.qll +++ b/python/ql/lib/semmle/python/Concepts.qll @@ -106,7 +106,8 @@ module FileSystemWriteAccess { } /** - * A data-flow node that may set or unset Cross-site request forgery protection. + * A data-flow node that may set or unset Cross-site request forgery protection + * in a global manner. * * Extend this class to refine existing API models. If you want to model new APIs, * extend `CSRFProtectionSetting::Range` instead. @@ -122,7 +123,8 @@ class CSRFProtectionSetting extends DataFlow::Node instanceof CSRFProtectionSett /** Provides a class for modeling new CSRF protection setting APIs. */ module CSRFProtectionSetting { /** - * A data-flow node that may set or unset Cross-site request forgery protection. + * A data-flow node that may set or unset Cross-site request forgery protection + * in a global manner. * * Extend this class to model new APIs. If you want to refine existing API models, * extend `CSRFProtectionSetting` instead. @@ -136,6 +138,39 @@ module CSRFProtectionSetting { } } +/** + * A data-flow node that provides Cross-site request forgery protection + * for a specific part of an application. + * + * Extend this class to refine existing API models. If you want to model new APIs, + * extend `CSRFProtection::Range` instead. + */ +class CSRFProtection extends DataFlow::Node instanceof CSRFProtection::Range { + /** + * Gets a `Function` representing the protected interaction + * (probably a request handler). + */ + Function getProtected() { result = super.getProtected() } +} + +/** Provides a class for modeling new CSRF protection setting APIs. */ +module CSRFProtection { + /** + * A data-flow node that provides Cross-site request forgery protection + * for a specific part of an application. + * + * Extend this class to model new APIs. If you want to refine existing API models, + * extend `CSRFProtection` instead. + */ + abstract class Range extends DataFlow::Node { + /** + * Gets a `Function` representing the protected interaction + * (probably a request handler). + */ + abstract Function getProtected(); + } +} + /** Provides classes for modeling path-related APIs. */ module Path { /** diff --git a/python/ql/lib/semmle/python/frameworks/Django.qll b/python/ql/lib/semmle/python/frameworks/Django.qll index ee273a9f2a6..baa81c682ea 100644 --- a/python/ql/lib/semmle/python/frameworks/Django.qll +++ b/python/ql/lib/semmle/python/frameworks/Django.qll @@ -2346,3 +2346,20 @@ module PrivateDjango { } } } + +private class DjangoCSRFDecorator extends CSRFProtection::Range { + Function function; + + DjangoCSRFDecorator() { + this = + API::moduleImport("django") + .getMember("views") + .getMember("decorators") + .getMember("csrf") + .getMember("csrf_protect") + .getAUse() and + this.asExpr() = function.getADecorator() + } + + override Function getProtected() { result = function } +} diff --git a/python/ql/src/Security/CWE-352/CSRFProtectionDisabled.ql b/python/ql/src/Security/CWE-352/CSRFProtectionDisabled.ql index 00f2cad5050..489ed1ea53c 100644 --- a/python/ql/src/Security/CWE-352/CSRFProtectionDisabled.ql +++ b/python/ql/src/Security/CWE-352/CSRFProtectionDisabled.ql @@ -15,5 +15,7 @@ import python import semmle.python.Concepts from CSRFProtectionSetting s -where s.getVerificationSetting() = false +where + s.getVerificationSetting() = false and + not exists(CSRFProtection p) select s, "Potential CSRF vulnerability due to forgery protection being disabled or weakened." diff --git a/python/ql/test/library-tests/frameworks/django-v2-v3/response_test.py b/python/ql/test/library-tests/frameworks/django-v2-v3/response_test.py index 74f306e8357..4007b2d8063 100644 --- a/python/ql/test/library-tests/frameworks/django-v2-v3/response_test.py +++ b/python/ql/test/library-tests/frameworks/django-v2-v3/response_test.py @@ -1,5 +1,6 @@ from django.http.response import HttpResponse, HttpResponseRedirect, HttpResponsePermanentRedirect, JsonResponse, HttpResponseNotFound from django.views.generic import RedirectView +from django.views.decorators.csrf import csrf_protect import django.shortcuts import json @@ -117,6 +118,7 @@ class CustomJsonResponse(JsonResponse): def __init__(self, banner, content, *args, **kwargs): super().__init__(content, *args, content_type="text/html", **kwargs) +@csrf_protect def safe__custom_json_response(request): return CustomJsonResponse("ACME Responses", {"foo": request.GET.get("foo")}) # $HttpResponse mimetype=application/json MISSING: responseBody=Dict SPURIOUS: responseBody="ACME Responses"