Skip to content

Latest commit

 

History

History
200 lines (183 loc) · 15.8 KB

File metadata and controls

200 lines (183 loc) · 15.8 KB

Vulnerability List

  • Vulnerability Name: Incomplete cache invalidation for TRUNCATE TABLE

  • Description: When a TRUNCATE TABLE command is executed via raw SQL, cachalot's raw SQL invalidation mechanism fails to detect it because the regular expression used to identify data-modifying SQL commands does not include the TRUNCATE TABLE keyword. This results in stale cache entries for queries involving the truncated table.

    Steps to trigger vulnerability:

    1. Application executes a database query through Django ORM that is cached by cachalot.
    2. An attacker, or an internal process, executes a raw SQL query TRUNCATE TABLE <table_name>; targeting the table involved in the cached query. This can be done through a raw SQL execution vulnerability in the application or directly to the database if attacker has access.
    3. Application executes the same database query again through Django ORM.
  • Impact: Stale cache data. Subsequent queries might return cached results that are inconsistent with the database state after the TRUNCATE TABLE operation. This could lead to incorrect application behavior, data integrity issues, and potential information disclosure if cached data is sensitive or if application logic relies on up-to-date data.

  • Vulnerability Rank: High

  • Currently Implemented Mitigations: None for TRUNCATE TABLE. Cachalot attempts to invalidate cache for raw SQL data modification using a regular expression in cachalot/monkey_patch.py, but this regex is incomplete and does not include TRUNCATE TABLE.

  • Missing Mitigations: Modify the regular expression SQL_DATA_CHANGE_RE in cachalot/monkey_patch.py to include the truncate keyword. This will ensure that TRUNCATE TABLE commands executed via raw SQL are detected and trigger cache invalidation for the affected tables.

  • Preconditions:

    1. Cachalot is enabled in the Django project.
    2. Queries involving a specific database table are being cached.
    3. There is a way for an attacker or internal process to execute raw SQL queries against the database, specifically TRUNCATE TABLE commands, targeting tables that are being cached by cachalot.
  • Source Code Analysis:

    1. File: /code/cachalot/monkey_patch.py
    2. Locate the _patch_cursor function and the SQL_DATA_CHANGE_RE regular expression:
    SQL_DATA_CHANGE_RE = re.compile(
        '|'.join([
            fr'(\W|\A){re.escape(keyword)}(\W|\Z)'
            for keyword in ['update', 'insert', 'delete', 'alter', 'create', 'drop']
        ]),
        flags=re.IGNORECASE,
    )
    
    def _patch_cursor_execute(original):
        @wraps(original)
        def inner(cursor, sql, *args, **kwargs):
            try:
                return original(cursor, sql, *args, **kwargs)
            finally:
                connection = cursor.db
                if getattr(connection, 'raw', True):
                    if isinstance(sql, bytes):
                        sql = sql.decode('utf-8')
                    sql = sql.lower()
                    if SQL_DATA_CHANGE_RE.search(sql): # Vulnerability: 'truncate' is missing in regex
                        tables = filter_cachable(
                            _get_tables_from_sql(connection, sql))
                        if tables:
                            invalidate(
                                *tables, db_alias=connection.alias,
                                cache_alias=cachalot_settings.CACHALOT_CACHE)
    1. The SQL_DATA_CHANGE_RE regex is intended to detect data-modifying SQL commands in raw SQL queries. However, the keyword list ['update', 'insert', 'delete', 'alter', 'create', 'drop'] does not include truncate.
    2. When a raw SQL query like TRUNCATE TABLE <table_name>; is executed, SQL_DATA_CHANGE_RE.search(sql.lower()) will return None because 'truncate' is not in the regex.
    3. As a result, the invalidate() function is not called, and the cache related to <table_name> is not invalidated, leading to stale cache data.
  • Security Test Case:

    1. Setup:
      • Ensure Django project is set up with cachalot enabled and a cache backend configured (e.g., locmem).
      • Define a Django model, for example, the Test model from /code/cachalot/tests/models.py.
      • Create an instance of the Test model in the database.
    2. Test Steps:
      • Execute a query that should be cached. For example, in Django shell:
        from cachalot.tests.models import Test
        list(Test.objects.all()) # First execution, should hit database
      • Verify that the query is cached by executing it again and checking the number of database queries.
        from django.test import TestCase
        class MyTest(TestCase):
            def test_cache_hit(self):
                with self.assertNumQueries(0): # Expecting 0 queries as it's cached
                    list(Test.objects.all())
      • Execute a raw SQL TRUNCATE TABLE command targeting the table of the Test model. Get the table name from Test._meta.db_table. In Django shell:
        from django.db import connection
        table_name = Test._meta.db_table
        with connection.cursor() as cursor:
            cursor.execute(f'TRUNCATE TABLE {table_name};')
      • Execute the cached query again:
        class MyTest(TestCase):
            def test_stale_cache_after_truncate(self):
                with self.assertNumQueries(0): # Still expecting 0 queries - STALE CACHE!
                    result = list(Test.objects.all())
                self.assertNotEqual(len(result), 0) # Expecting stale data, not empty result
      • Assert that the result of the query is still served from the cache (0 database queries) and that the result is not empty, indicating stale data despite the table being truncated.
    3. Expected Result (Vulnerability): The test test_stale_cache_after_truncate should pass, demonstrating that the cache is stale after TRUNCATE TABLE. The number of queries should be 0, and the result should not be empty, proving the vulnerability.
    4. Mitigation (To fix the vulnerability): Modify cachalot/monkey_patch.py and add 'truncate' to the keywords list in SQL_DATA_CHANGE_RE definition.
    5. Retest after Mitigation: After applying the mitigation, rerun the security test case. The test test_stale_cache_after_truncate should now fail in the intended way, or the assertion should be adjusted to expect 1 query and an empty result, confirming that the cache is correctly invalidated after TRUNCATE TABLE.

  • Vulnerability Name: Arbitrary Code Execution via Unsanitized Eval in the Benchmark Module
    • Description:
      • The benchmark module (in the previously reviewed benchmark.py file) constructs a Python lambda function by concatenating a fixed string with a parameter (query_str).
      • This concatenated string is then passed unchecked to an unsanitized eval call.
      • If an adversary can somehow supply or influence the value of query_str—for example, via an accidental exposure of the module as an HTTP endpoint or misuse in a production-like configuration—they will be able to inject and execute arbitrary Python code. Steps to trigger vulnerability:
      1. Attacker finds a way to influence or supply the query_str parameter, potentially through a misconfigured endpoint or exposed benchmark functionality.
      2. The benchmark module's code uses eval to execute a lambda function constructed using the attacker-controlled query_str.
      3. Arbitrary Python code injected within query_str is executed on the server.
    • Impact:
      • Successful exploitation would allow an attacker to execute arbitrary code on the server, potentially reading, modifying, or deleting sensitive data and even leading to full system takeover.
    • Vulnerability Rank: Critical
    • Currently Implemented Mitigations:
      • In the current source, the benchmark queries are hard‑coded inside the execute_benchmark() method, and the module is intended only for performance testing (not production use).
    • Missing Mitigations:
      • No input sanitization or safe parsing (for example, using a restricted evaluation context or a safe expression parser) is applied to the dynamic string before it is evaluated.
      • There is no access control preventing this module from being accidentally exposed via a public endpoint.
    • Preconditions:
      • The benchmark module must be deployed or inadvertently exposed in a production environment.
      • An attacker must be able to supply a manipulated value for query_str (for example, through a misconfigured view or API endpoint that calls the benchmarking code).
    • Source Code Analysis:
      • In the method that builds the query, the code simply prepends "Test.objects.using(using)" to the provided query_str.
      • If the query is wrapped (e.g. via list( …) when a flag is true), the resulting string is still formed directly from user-controlled input.
      • This string is then concatenated into an expression passed to eval("lambda using: " + query_str) without any filtering or sanitization, meaning an attacker could insert arbitrary Python code.
    • Security Test Case:
      • Deploy the application in a secure test environment where the benchmark module is accessible (for example, via a debug endpoint).
      • Craft an HTTP request or simulate the call by supplying a suspect query_str payload such as:
        ".count() or __import__('os').system('echo vulnerable > /tmp/owned.txt')"
        
      • Invoke the benchmark method and check the system for the creation of /tmp/owned.txt or the execution of other measurable side‐effects to confirm that injected code is executed.

  • Vulnerability Name: Default Database Credentials Vulnerability
    • Description:
      • In the project’s configuration (as seen in the previously reviewed settings.py), insecure defaults are specified for database connections.
      • For PostgreSQL, the password is hard‑coded as "password", and for MySQL an empty password is allowed.
      • Although environment variables (e.g. POSTGRES_PASSWORD and MYSQL_PASSWORD) can override these defaults, if a deployment uses the default configurations the insecure credentials remain in effect. Steps to trigger vulnerability:
      1. Application is deployed using default settings without overriding environment variables for database credentials.
      2. An attacker gains network access to the database server due to lax network restrictions or misconfiguration.
      3. Attacker attempts to connect to the PostgreSQL instance using the default password "password" or to the MySQL instance with an empty password.
    • Impact:
      • An external attacker able to reach the database (for instance, if network restrictions are lax) can leverage these default credentials to gain unauthorized access.
      • The attacker could then exfiltrate or tamper with sensitive backend data, potentially leading to a full compromise of the backend database infrastructure.
    • Vulnerability Rank: High
    • Currently Implemented Mitigations:
      • The project permits credentials to be overridden through environment variables; however, nothing enforces that these defaults must be changed in production deployments.
    • Missing Mitigations:
      • There is no runtime check or warning to ensure that insecure default credentials are not used.
      • No deployment‐time configuration management is in place to enforce the use of secure credentials on publicly accessible instances.
    • Preconditions:
      • The application is deployed using the default settings without environment variable overrides.
      • The underlying database servers are accessible to external attackers (for example, via misconfigured network/firewall settings).
    • Source Code Analysis:
      • In settings.py, the PostgreSQL configuration is hard-coded with PASSWORD: 'password' and MySQL is configured to allow an empty password (as indicated by the flag MYSQL_ALLOW_EMPTY_PASSWORD: yes).
      • Although the code checks for environment variable overrides, no mechanism enforces that these insecure defaults are replaced before a production deployment.
    • Security Test Case:
      • In a controlled test environment, deploy the application without setting the overriding environment variables.
      • From an external system, attempt to connect to the PostgreSQL and MySQL instances using the default credentials.
      • Verify that the connection succeeds and that the attacker can read or list databases, confirming the vulnerability.

  • Vulnerability Name: Debug Toolbar Information Disclosure Vulnerability
    • Description:
      • The project configuration (including entries in INSTALLED_APPS and URL routing in, for example, runtests_urls.py) enables the Django debug toolbar unconditionally when DEBUG mode is active.
      • Although access is nominally restricted by setting INTERNAL_IPS = ['127.0.0.1'], if the application is deployed with DEBUG=True or if a reverse proxy or network mis‑configuration permits spoofing of the internal IP check, an external attacker could access the debug toolbar.
      • In the project’s test file (debug_toolbar.py), the toolbar is rendered on the root URL, and its panels (which include detailed runtime and SQL query information) are accessible. Steps to trigger vulnerability:
      1. Application is deployed in production with DEBUG=True.
      2. An attacker accesses the application through a web browser.
      3. Attacker navigates to debug toolbar URLs (e.g., /__debug__/).
      4. If INTERNAL_IPS check is bypassed due to misconfiguration or header spoofing, the debug toolbar is rendered.
    • Impact:
      • Disclosure of the debug toolbar exposes sensitive runtime details, such as SQL queries, settings, and cache states.
      • This detailed internal information can help an attacker craft further attacks by revealing application structure and behavior.
    • Vulnerability Rank: High
    • Currently Implemented Mitigations:
      • The configuration restricts toolbar access by setting INTERNAL_IPS to ['127.0.0.1'], which under normal conditions should only permit local requests.
    • Missing Mitigations:
      • There is no explicit enforcement that DEBUG (and the debug toolbar) is disabled in production environments.
      • No advanced access controls (e.g. proper handling of proxy headers or additional authentication) are implemented to ensure that remote requests cannot bypass the internal IP check.
    • Preconditions:
      • The application must be deployed with DEBUG=True (or with the debug toolbar enabled) in a production environment.
      • An attacker must be able to bypass or spoof the INTERNAL_IPS restriction (for example, via a misconfigured reverse proxy or by manipulating request headers like X-Forwarded-For).
    • Source Code Analysis:
      • In the project’s settings, the debug toolbar is added to both INSTALLED_APPS and MIDDLEWARE, and the URL configuration (as exemplified by runtests_urls.py) routes requests beginning with /__debug__/ to the toolbar.
      • The simplistic use of an internal IP check means that if an attacker can present a spoofed internal IP address, the detailed debugging interface becomes available.
    • Security Test Case:
      • Deploy the application in a staging environment with DEBUG=True.
      • From an external machine, attempt to access the /__debug__/ URL.
      • Then modify request headers (for example, setting X-Forwarded-For to 127.0.0.1) and repeat the request.
      • Verify whether the debug toolbar is rendered and examine the page for detailed internal information (such as SQL logs, setting values, and cache details).
      • Successful access confirms the vulnerability and highlights the need to disable the toolbar outside of secure local development.