-
Vulnerability name: CSRF Protection Misconfiguration Vulnerability
-
Description: An attacker can induce a state-changing action by luring an authenticated user to a malicious webpage that automatically submits a forged request. This vulnerability occurs when CSRF protection is disabled or misconfigured in a Django Ninja application that uses cookie-based authentication.
- An attacker crafts a malicious website containing a Cross-Site Request Forgery (CSRF) attack.
- A user authenticates to a Django Ninja application that uses cookie-based authentication (e.g., Django sessions, APIKeyCookie). This results in the application setting a session cookie in the user's browser.
- The developer of the Django Ninja application, either unknowingly or intentionally, disables CSRF protection. This can be done by setting
csrf=False
duringNinjaAPI
initialization. - While the user is logged into the Django Ninja application and their session cookie is active, they visit the attacker's malicious website in the same browser.
- The malicious website, without the user's awareness, sends a cross-site request to the Django Ninja application. This request is designed to perform an action, such as modifying data or executing administrative functions, that the attacker intends. The browser automatically includes the Django Ninja application's session cookie with this cross-site request.
- Because CSRF protection is disabled in the Django Ninja application (due to
csrf=False
), the application does not perform standard CSRF token validation to verify the request's origin. - The Django Ninja application only verifies the presence of a valid session cookie. Since the browser automatically sent the session cookie, the application considers the request to be authenticated.
- The Django Ninja application processes the attacker's malicious request as if it were a legitimate user action, leading to the execution of unauthorized actions.
- The attacker successfully performs actions on behalf of the logged-in user, potentially leading to account compromise, data breaches, or unintended operations within the application.
-
Impact:
- Account Takeover: An attacker could change user passwords or email addresses, gaining control over user accounts.
- Data Manipulation: Critical data could be modified, deleted, or corrupted, leading to data integrity issues or loss.
- Unauthorized Transactions: Users might be unknowingly made to perform actions like financial transactions or data transfers.
- Privilege Escalation: If an administrative account is targeted, attackers could gain complete control over the Django Ninja application and its data.
-
Vulnerability rank: High
-
Currently implemented mitigations:
- Django Ninja framework has an automatic CSRF protection mechanism. When cookie-based authentication methods like
APIKeyCookie
,SessionAuth
, ordjango_auth
are used, CSRF protection is automatically enabled by default. This default behavior is implemented in theNinjaAPI
constructor inninja/main.py
and documented in/code/docs/docs/reference/csrf.md
and/code/docs/docs/whatsnew_v1.md
. - The
APIKeyCookie
security class inninja/security/apikey.py
, which is the base forSessionAuth
andSessionAuthSuperUser
inninja/security/session.py
, includes CSRF checks by default. The__init__
method ofAPIKeyCookie
setsself.csrf = csrf
with a default value ofTrue
, and the_get_key
method callscheck_csrf(request)
ifself.csrf
is True. - The framework’s documentation and sample code explicitly warn that CSRF protection is disabled by default when
csrf=False
is set, and show how to enable it (by settingcsrf=True
or relying on default behavior when using cookie-based auth). - A deprecation warning is issued in
ninja/main.py
when thecsrf
argument is used, alerting developers that CSRF is now handled via the auth mechanism—but without enforcing a secure default.
- Django Ninja framework has an automatic CSRF protection mechanism. When cookie-based authentication methods like
-
Missing mitigations:
- No explicit runtime warning for intentional CSRF disabling: If a developer explicitly sets
csrf=False
inNinjaAPI
constructor while also using cookie-based authentication, there is no runtime warning or error raised to highlight the security implications. This lack of immediate feedback might lead to unintentional exposure to CSRF attacks if developers are not fully aware of the risks of disabling CSRF in such setups. - Documentation prominence and stronger warning: While CSRF protection is documented, the documentation could be improved by:
- Placing a more prominent warning in the CSRF documentation section about the risks of disabling CSRF, specifically when using cookie-based authentication.
- Adding a strong recommendation against disabling CSRF unless for very specific use-cases (like public APIs with non-browser clients only) and with a clear understanding of the security consequences.
- Including guidance on alternative approaches if developers believe they have reasons to disable CSRF, encouraging them to reconsider or implement other security measures.
- No runtime enforcement exists to automatically enable CSRF protection when cookie‑based authentication is used and
csrf=False
is explicitly set. - A more secure default (or a production‑time warning) would help prevent inadvertent deployment with CSRF disabled when using cookie-based authentication.
- No explicit runtime warning for intentional CSRF disabling: If a developer explicitly sets
-
Preconditions:
- Cookie-based Authentication in Use: The Django Ninja application must be configured to use a cookie-based authentication mechanism (e.g., Django sessions,
APIKeyCookie
,SessionAuth
). - CSRF Protection Explicitly Disabled: The
NinjaAPI
constructor must be initialized withcsrf=False
. - Active User Session: A user must be logged into the Django Ninja application, possessing a valid session cookie in their browser.
- User Interaction with External Malicious Content: The logged-in user must visit a website or interact with content controlled by the attacker (e.g., through a link in an email or visiting a malicious site) that is designed to execute a CSRF attack against the Django Ninja application.
- The API is publicly accessible and deployed using the
csrf=False
setting while relying on cookie‑ or session‑based authentication.
- Cookie-based Authentication in Use: The Django Ninja application must be configured to use a cookie-based authentication mechanism (e.g., Django sessions,
-
Source code analysis:
ninja/main.py
:- The
NinjaAPI
class constructor handles thecsrf
argument. It automatically enables CSRF protection ifauth
is provided andcsrf
is not explicitly set toFalse
.
def __init__( self, *, ..., csrf: bool = False, auth: Optional[Union[Sequence[Callable], Callable, NOT_SET_TYPE]] = NOT_SET, ... ): ... if auth is not NOT_SET and not csrf: # auto csrf self.csrf = True else: self.csrf = csrf ...
- The
ninja/security/apikey.py
:- The
APIKeyCookie
class initializescsrf
toTrue
by default and uses thecheck_csrf
utility function to validate CSRF tokens ifcsrf
is enabled.
class APIKeyCookie(APIKeyBase, ABC): openapi_in: str = "cookie" def __init__(self, csrf: bool = True) -> None: self.csrf = csrf super().__init__() def _get_key(self, request: HttpRequest) -> Optional[str]: if self.csrf: error_response = check_csrf(request) if error_response: raise HttpError(403, "CSRF check Failed") return request.COOKIES.get(self.param_name)
- The
ninja/utils.py
:- The
check_csrf
function leverages Django'sCsrfViewMiddleware
to perform CSRF validation.
def check_csrf( request: HttpRequest, callback: Callable = _no_view ) -> Optional[HttpResponseForbidden]: mware = CsrfViewMiddleware(lambda x: HttpResponseForbidden()) # pragma: no cover request.csrf_processing_done = False # type: ignore mware.process_request(request) return mware.process_view(request, callback, (), {})
- The
docs/docs/reference/csrf.md
anddocs/docs/whatsnew_v1.md
:- Documentation confirms the default CSRF protection behavior and provides information on how to configure and use CSRF protection in Django Ninja.
-
Security test case:
- Set up a vulnerable Django Ninja API application:
- Define a Django Ninja API endpoint that is protected with cookie-based authentication. For instance, use
APIKeyCookie
or Django's session authentication (django_auth
). - Initialize the
NinjaAPI
instance with CSRF protection explicitly disabled:api = NinjaAPI(csrf=False, auth=...)
. - Create a POST endpoint, for example
/api/change_email/
, that modifies a user-related attribute (like email). This endpoint should be protected by the configured cookie-based authentication.
- Define a Django Ninja API endpoint that is protected with cookie-based authentication. For instance, use
- Develop a malicious HTML website for CSRF attack:
- Create an HTML file (can be hosted locally or on a separate server).
- In the HTML body, include a form that automatically submits a POST request to the
/api/change_email/
endpoint of the Django Ninja application upon page load (using JavaScript for auto-submission). - The form should include a field (e.g.,
new_email
) with a value chosen by the attacker to demonstrate the exploit.
<html> <head> <title>CSRF Attack</title> </head> <body> <h1>CSRF Attack!</h1> <form id="csrf-form" action="http://your-django-ninja-app.com/api/change_email/" method="POST"> <input type="hidden" name="new_email" value="[email protected]"> </form> <script> document.getElementById('csrf-form').submit(); </script> </body> </html>
- Replace
http://your-django-ninja-app.com/api/change_email/
with the actual URL of your vulnerable endpoint.
- Authenticate as a user:
- Open a web browser and log in to the Django Ninja application with valid user credentials. This establishes a session and stores the session cookie in the browser.
- Access the malicious website:
- In the same browser session where you are logged into the Django Ninja application, navigate to the malicious HTML file you created.
- Observe the successful CSRF exploit:
- Upon loading the malicious HTML page, the embedded form will automatically submit a POST request to the Django Ninja application's
/api/change_email/
endpoint. - Verify that the email address associated with the logged-in user in the Django Ninja application has been changed to
[email protected]
. This confirms the CSRF attack was successful because the application processed the unauthorized request due to the disabled CSRF protection and the presence of the valid session cookie.
- Upon loading the malicious HTML page, the embedded form will automatically submit a POST request to the Django Ninja application's
- Set up a vulnerable Django Ninja API application:
- Vulnerability name: Debug Mode Information Disclosure Vulnerability
- Description:
When Django’s
DEBUG
setting is left enabled in production, unhandled exceptions trigger error responses that include detailed tracebacks. Attackers can deliberately send malformed or invalid input to API endpoints to trigger exceptions. The full traceback—including file paths, code snippets, and configuration information—is then returned in the HTTP response. - Impact: Sensitive internal details (such as source code layout, installed modules, and framework configuration) are disclosed, aiding further targeted attacks and simplifying automated vulnerability scanning.
- Vulnerability rank: High
- Currently implemented mitigations:
- Best practices documented by Django and reinforced by test files specify that
DEBUG
must be set to False in production. - The default exception handler (in
ninja/errors.py
) sanitizes error output only whenDEBUG
is False.
- Best practices documented by Django and reinforced by test files specify that
- Missing mitigations:
- There is no safe‑by‑default mode; the framework relies entirely on the developer to set
DEBUG=False
in production. - An additional safeguard or runtime warning when detailed error output is detected in a production setting is missing.
- There is no safe‑by‑default mode; the framework relies entirely on the developer to set
- Preconditions:
- The API instance is deployed with Django’s
DEBUG=True
, and an attacker is able to trigger an unhandled exception on a public endpoint.
- The API instance is deployed with Django’s
- Source code analysis:
- In
ninja/errors.py
, the_default_exception()
function returns the full traceback (viatraceback.format_exc()
) as plain text whensettings.DEBUG
is True. - The helper function
debug_server_url_reimport()
inninja/main.py
is used to detect development‑mode re‐imports but does not mitigate the exposure of sensitive error details.
- In
- Security test case:
- Deploy the API with
DEBUG=True
(mimicking a production misconfiguration). - Identify an endpoint and send a request with invalid JSON or deliberately malformed data to trigger an exception.
- Capture the HTTP response and inspect its body to verify that it contains a detailed traceback with internal file paths and configuration details.
- Expected Outcome: The response discloses the complete traceback, confirming the information disclosure vulnerability.
- Deploy the API with
- Vulnerability name: Insufficient Rate Limiting on Authentication Endpoints Vulnerability
- Description:
Throttling classes are available within the framework (such as
AnonRateThrottle
,AuthRateThrottle
, andUserRateThrottle
defined inninja/throttling.py
); however, if developers do not explicitly configure throttle limits on sensitive endpoints (for instance, those validating API keys), then no rate limiting is enforced. This lack of default protections allows attackers to script rapid, repeated authentication attempts (brute‑forcing credentials) with minimal delay. - Impact: Attackers might guess valid API keys or credentials by exploiting the absence of conservative rate limits, leading to unauthorized access.
- Vulnerability rank: High
- Currently implemented mitigations:
- The framework provides robust, configurable throttle classes and includes tests that demonstrate their functionality when explicitly applied.
- Missing mitigations:
- There is no secure‑by‑default throttle configuration for authentication endpoints; if developers overlook specifying throttle objects (leaving the throttle attribute as NOT_SET), the endpoints remain open to rapid repeated requests.
- A default conservative rate limit (e.g. a few attempts per minute per IP) would mitigate brute‑force risks.
- Preconditions:
- The API is publicly accessible on endpoints that use API Key (or other sensitive) authentication, and no explicit throttling is configured—thus the default (NOT_SET) throttle is in effect.
- Source code analysis:
- In
ninja/throttling.py
, theSimpleRateThrottle
class’sallow_request()
method checks the request history stored in the cache. When no throttle is attached (or the developer leaves throttle as NOT_SET), there is no enforcement to limit the rate of incoming requests. - Test modules illustrate that when explicit throttle objects are not provided, the API processes authentication requests without delays or rate limits.
- In
- Security test case:
- Create and deploy an API endpoint that uses API Key–based authentication, ensuring that no throttle object is configured (i.e. throttle remains as NOT_SET).
- Using an automated script, send a large number of authentication requests with incorrect API keys from one or more IP addresses.
- Observe that the API processes every request immediately with no throttling (i.e. no HTTP 429 status responses).
- Expected Outcome: The absence of default rate limiting allows rapid repeated requests, facilitating brute‑force attacks.
- Vulnerability name: Public Exposure of OpenAPI Documentation Vulnerability
- Description:
By default, the NinjaAPI instance (see
ninja/main.py
) is configured withdocs_url
set to “/docs” andopenapi_url
set to “/openapi.json”. The OpenAPI specification and interactive documentation are then added to Django’s URL configuration (seeninja/openapi/urls.py
) without any authentication or access restrictions. An unauthenticated attacker can directly access these endpoints to retrieve detailed information on the API’s routes, parameters, and models. - Impact: Full exposure of the API’s internal structure can enable attackers to map out endpoints and craft more sophisticated, targeted attacks; it also simplifies automated vulnerability scanning.
- Vulnerability rank: High
- Currently implemented mitigations:
- The framework supports disabling these documentation endpoints by setting
docs_url=None
and/oropenapi_url=None
, and it provides the option to wrap the endpoints using an authentication decorator (see documentation examples).
- The framework supports disabling these documentation endpoints by setting
- Missing mitigations:
- Out‑of‑the‑box, documentation endpoints remain enabled and are publicly accessible, leaving the API fully documented without any access control.
- A secure‑by‑default behavior (such as restricting access in production) would considerably reduce the attack surface.
- Preconditions:
- The API instance is deployed with the default configuration where
docs_url
andopenapi_url
are enabled and not protected by any authentication or authorization mechanism.
- The API instance is deployed with the default configuration where
- Source code analysis:
- In
ninja/main.py
, the constructor sets default values fordocs_url
("/docs") andopenapi_url
("/openapi.json"). - In
ninja/openapi/urls.py
, these endpoints are automatically added to the URL configuration without any built‑in safeguards.
- In
- Security test case:
- Deploy the API using the default configuration (i.e. with
docs_url
andopenapi_url
enabled). - From an external, unauthenticated network, access the endpoints “/docs” and “/openapi.json” using a web browser or a HTTP client.
- Verify that the full OpenAPI specification is disclosed, revealing endpoints, parameter definitions, and even default values.
- Expected Outcome: The API documentation is accessible without any form of authentication, thereby confirming the vulnerability.
- Deploy the API using the default configuration (i.e. with
-
Vulnerability name: Throttling Bypass via X-Forwarded-For Header Manipulation
-
Description: An attacker can bypass IP-based throttling mechanisms (like
AnonRateThrottle
,UserRateThrottle
) by manipulating theX-Forwarded-For
HTTP header. This vulnerability occurs because the application might not be correctly configured to handle requests behind a proxy, specifically regarding the number of proxies (NUM_PROXIES
setting). IfNUM_PROXIES
is not set or incorrectly set, the system might use the attacker-controlled IP address from theX-Forwarded-For
header instead of the actual client IP address for throttling.Steps to trigger vulnerability:
- Application is deployed behind a proxy (e.g., CDN, load balancer).
- Throttling is implemented using
AnonRateThrottle
orUserRateThrottle
which rely on IP address for rate limiting. - Attacker sends multiple requests to the application, including a crafted
X-Forwarded-For
header with a spoofed IP address. - If
NUM_PROXIES
setting is not properly configured to reflect the number of proxies in front of the application, the throttling mechanism will use the spoofed IP fromX-Forwarded-For
instead of the actual client IP. - Attacker can bypass throttling by changing the spoofed IP address in subsequent requests, as the system will treat each request as coming from a different IP.
-
Impact: Successful exploitation of this vulnerability allows attackers to bypass rate limiting, potentially leading to:
- Brute-force attacks: Attackers can make unlimited login attempts or other security-sensitive actions without being throttled.
- Resource exhaustion: Attackers can send a high volume of requests, overwhelming the server and potentially leading to service disruptions or increased operational costs.
- Circumvention of security measures: Throttling is often used as a security measure to protect against various attacks. Bypassing it weakens the overall security posture of the application.
-
Vulnerability rank: High
-
Currently implemented mitigations: The
SimpleRateThrottle
class inninja/throttling.py
includes logic to handle proxy headers using theNUM_PROXIES
setting fromninja.conf.settings
.def get_ident(self, request: HttpRequest) -> Optional[str]: xff = request.META.get("HTTP_X_FORWARDED_FOR") if xff: xff_hosts = xff.split(",") num_proxies = settings.NUM_PROXIES if num_proxies is None: return xff_hosts[-1].strip() # default behavior elif num_proxies >= 1: return xff_hosts[-(num_proxies + 1)].strip() # last proxy addr in the list else: # num_proxies == 0 return xff_hosts[0].strip() # client addr (first in the list) return request.META.get("REMOTE_ADDR")
This code attempts to retrieve the correct client IP based on
NUM_PROXIES
. However, misconfiguration ofNUM_PROXIES
leads to vulnerability. -
Missing mitigations:
- Configuration Guidance and Best Practices: The project lacks clear documentation and warnings about the importance of correctly configuring
NUM_PROXIES
when deploying behind proxies. This should include guidelines on how to determine the correct value forNUM_PROXIES
based on the deployment environment. - Automatic Proxy Detection (Optional but Recommended): While not always feasible, exploring options for automatic detection of proxy setups or providing tools to help administrators determine the correct
NUM_PROXIES
value could improve security. - Rate Limiting based on other factors: Consider supplementing or offering alternatives to solely IP-based throttling, such as token-based or user-account based throttling, which are less susceptible to IP spoofing.
- Configuration Guidance and Best Practices: The project lacks clear documentation and warnings about the importance of correctly configuring
-
Preconditions:
- Django Ninja application is deployed behind at least one proxy server (e.g., CDN, load balancer, reverse proxy).
- IP-based throttling is enabled using
AnonRateThrottle
orUserRateThrottle
. - The
NUM_PROXIES
setting in Django settings is either not set, set toNone
(default, which might be insecure in proxy setups), or incorrectly configured for the actual number of proxies.
-
Source code analysis:
ninja/throttling.py
-SimpleRateThrottle.get_ident()
:- The
get_ident
method retrieves the client's IP address. - It first checks for the
HTTP_X_FORWARDED_FOR
header fromrequest.META
. - If the header is present, it splits the header value by commas into a list of IP addresses (
xff_hosts
). - It retrieves the
NUM_PROXIES
setting fromninja.conf.settings
. - Case 1:
num_proxies is None
(Default): It returnsxff_hosts[-1].strip()
, which is the last IP address in theX-Forwarded-For
header. In a typical proxy setup, the last IP is usually the proxy's IP, not the client's original IP. This is the default behavior and is vulnerable ifNUM_PROXIES
is not configured when behind proxies. - Case 2:
num_proxies >= 1
: It returnsxff_hosts[-(num_proxies + 1)].strip()
. This attempts to get the client IP by going backnum_proxies + 1
hops in theX-Forwarded-For
list. For example, ifNUM_PROXIES = 1
, it takes the second to last IP. This is intended for setups with a known number of proxies. - Case 3:
num_proxies == 0
: It returnsxff_hosts[0].strip()
, which is the first IP in theX-Forwarded-For
list. This is meant to be the client IP whenNUM_PROXIES
is set to 0, assuming the first IP is the originating client. - If the
HTTP_X_FORWARDED_FOR
header is not present, it falls back torequest.META.get("REMOTE_ADDR")
, which is the IP address of the immediate connection to the server (typically the proxy in a proxy setup, or the client directly if no proxy).
- The
-
Security test case:
- Setup: Deploy a Django Ninja application with IP-based throttling (
AnonRateThrottle
applied to a publicly accessible endpoint) behind an Nginx reverse proxy. Ensure the Django application is configured to use the defaultNUM_PROXIES = None
or explicitly set it toNone
. - Baseline Test: Send several requests from a single IP address to the throttled endpoint without the
X-Forwarded-For
header. Verify that after exceeding the rate limit, the server correctly applies throttling and returns 429 status codes. - Throttling Bypass Attempt via X-Forwarded-For:
- Use a tool like
curl
or a Python script to send requests to the throttled endpoint from the same source IP address used in the baseline test. - For each request, include the
X-Forwarded-For
header, crafting it to contain a list of IPs. The last IP in the list should be the IP address of your Nginx proxy server. The IP addresses before the proxy IP in the list should be spoofed, unique IP addresses. For example:X-Forwarded-For: 1.1.1.1, <Nginx_Proxy_IP>
,X-Forwarded-For: 1.1.1.2, <Nginx_Proxy_IP>
,X-Forwarded-For: 1.1.1.3, <Nginx_Proxy_IP>
, and so on. Increment the spoofed IP (1.1.1.x) for each subsequent request. - Observe the responses. If the vulnerability is present, the server will continue to respond with 200 OK even after exceeding the intended rate limit. This is because the default
NUM_PROXIES = None
configuration causesget_ident()
to use the last IP inX-Forwarded-For
(the proxy IP), or in some cases, the attacker-controlled spoofed IP, effectively bypassing the IP-based throttling.
- Use a tool like
- Verification of Mitigation:
- Correctly configure the Django Ninja application by setting
NUM_PROXIES = 1
(assuming there is one reverse proxy in front). - Repeat steps 2 and 3 (baseline and bypass attempts).
- With
NUM_PROXIES = 1
, the throttling should now be correctly applied based on the actual client IP address (which Ninja will extract from theX-Forwarded-For
header). The bypass attempt using spoofedX-Forwarded-For
headers should no longer be effective. After exceeding the rate limit, the server should return 429 status codes, even with manipulatedX-Forwarded-For
headers.
- Correctly configure the Django Ninja application by setting
- Setup: Deploy a Django Ninja application with IP-based throttling (