HIGH out of bounds readdjango

Out Of Bounds Read in Django

How Out Of Bounds Read Manifests in Django

An out‑of‑bounds (OOB) read occurs when a program accesses memory or data beyond the intended limits of a buffer, array, or collection. In Django applications this most often shows up when developer‑supplied numeric parameters (e.g., offset, limit, page number) are used directly to slice querysets, lists, strings, or file streams without proper validation.

Typical vulnerable patterns include:

  • Unchecked QuerySet slicing – a view takes start and length from request.GET and does MyModel.objects.all()[int(start):int(start)+int(length)]. If start is negative or excessively large, the database may return rows that were not meant to be exposed, or the underlying Python slice may raise an IndexError that leaks stack traces.
  • Raw SQL with user‑controlled LIMIT/OFFSET – using cursor.execute("SELECT * FROM my_table LIMIT %s OFFSET %s", [limit, offset]) where limit and offset come directly from the request. An attacker can set a huge offset to force the database to scan past the intended range, causing excessive data return or denial‑of‑service.
  • File streaming with user‑supplied byte range – a custom view that serves a FileField by reading file.open() and then file.seek(int(request.GET['start'])) followed by file.read(int(request.GET['length'])). Without bounds checks, a negative start or a length that exceeds the file size can cause the OS to return data from adjacent memory or trigger an error that reveals internal paths.
  • Pagination misuse – passing a user‑provided page parameter straight to Paginator(queryset, per_page).page(page). If the page number is zero or negative, Django’s paginator will raise an EmptyPage exception; catching it incorrectly and returning a default page can leak the first or last set of records.

Real‑world impact mirrors CVEs such as CVE-2022-23816 (out‑of‑bounds read in Python’s bytes object) and CVE-2021-32052 (Django URL validation bypass that can lead to excessive data exposure). In the API context, the weakness maps to OWASP API Security Top 10 2023 A3: Excessive Data Exposure, because an attacker can read more records than intended.

Django‑Specific Detection

Detecting OOB reads in Django requires looking at places where user‑controlled numeric values influence data access. Manual code review can spot the patterns above, but automated scanning speeds up the process.

When you submit an API endpoint to middleBrick, the scanner performs unauthenticated black‑box checks that include:

  • Sending a variety of numeric payloads (negative numbers, extremely large values, non‑numeric strings) in query parameters that are commonly used for pagination or slicing (offset, limit, page, start, end).
  • Analyzing responses for signs of excess data (e.g., more records than the expected page size) or error messages that reveal internal details (stack traces, file paths).
  • Checking the OpenAPI/Swagger specification (if present) for missing minimum, maximum, or exclusiveMinimum/exclusiveMaximum constraints on integer parameters that control slicing.
  • Flagging findings under the Input Validation category with a severity of high when the scanner confirms that altering the parameter changes the amount of data returned beyond the intended bounds.

Example of a middleBrick finding (JSON output from the CLI):

{
  "finding_id": "OB-001",
  "title": "Unvalidated offset parameter leads to out‑of‑bounds read",
  "description": "The endpoint /api/articles accepts an 'offset' query parameter that is used directly in a QuerySet slice without bounds checking. Supplying offset=-100 returns the last 100 articles, exposing data that should require authentication.",
  "severity": "high",
  "category": "Input Validation",
  "remediation": "Validate and clamp the offset to a non‑negative integer within the expected range before using it in a slice."
}

In addition to middleBrick, developers can run Bandit with the B101 (assert used) and B307 (eval) plugins, or use django‑security‑check to detect missing validation on integer fields in serializers.

Django‑Specific Remediation

Fixing OOB reads in Django centers on strict input validation and using Django’s built‑in helpers that enforce bounds.

1. **Validate and clamp numeric parameters** – convert to int inside a try/except block, reject non‑integers, and enforce minimum/maximum values.

# views.py
from django.http import JsonResponse, HttpResponseBadRequest
from django.core.paginator import Paginator, EmptyPage
from .models import Article

def article_list(request):
    try:
        start = int(request.GET.get('offset', 0))
        length = int(request.GET.get('limit', 20))
    except ValueError:
        return HttpResponseBadRequest('offset and limit must be integers')
    
    # Clamp to safe ranges
    if start < 0:
        start = 0
    if length < 1 or length > 100:
        length = 20
    
    queryset = Article.objects.all()
    # Safe slicing after validation
    articles = queryset[start:start + length]
    data = [{'id': a.id, 'title': a.title} for a in articles]
    return JsonResponse({'results': data})

2. **Leverage Django’s Paginator correctly** – always catch EmptyPage and return a proper error or the last page, never expose extra data.

def article_list_paginated(request):
    paginator = Paginator(Article.objects.all(), 20)
    page_num = request.GET.get('page')
    try:
        page = paginator.page(page_num)
    except (PageNotAnInteger, EmptyPage):
        # Return first page for invalid input
        page = paginator.page(1)
    
    data = [{'id': a.id, 'title': a.title} for a in page.object_list]
    return JsonResponse({'results': data, 'page': page.number, 'total_pages': paginator.num_pages})

3. **Use ORM methods that avoid raw slicing** – prefer filter() with date or ID ranges when possible, or use values()/only() to limit fields.

# Example: fetch articles by ID range after validation
try:
    min_id = int(request.GET.get('min_id', 1))
    max_id = int(request.GET.get('max_id', 100))
except ValueError:
    return HttpResponseBadRequest('IDs must be integers')

if min_id < 1 or max_id < min_id or max_id - min_id > 200:
    return HttpResponseBadRequest('Invalid ID range')

articles = Article.objects.filter(id__gte=min_id, id__lte=max_id).only('id', 'title')

4. **When serving files, validate byte ranges** – use Django’s FileResponse with the built‑in range handling or check that start and end are within file.size.

from django.http import FileResponse, HttpResponseNotFound
import os

def serve_attachment(request, file_id):
    try:
        attachment = Attachment.objects.get(id=file_id)
    except Attachment.DoesNotExist:
        return HttpResponseNotFound()
    
    try:
        start = int(request.GET.get('start', 0))
        end = int(request.GET.get('end', attachment.file.size - 1))
    except ValueError:
        return HttpResponseBadRequest('Invalid range')
    
    if start < 0 or end > attachment.file.size - 1 or start > end:
        return HttpResponseBadRequest('Range out of bounds')
    
    response = FileResponse(attachment.file.open('rb'), content_type=attachment.mime_type)
    response['Content-Range'] = f'bytes {start}-{end}/{attachment.file.size}'
    response['Content-Length'] = str(end - start + 1)
    return response

By applying these patterns—strict type conversion, range clamping, and using Django’s pagination and ORM safely—you eliminate the conditions that allow an out‑of‑bounds read to succeed.

Frequently Asked Questions

Can middleBrick detect out‑of‑bounds reads that only appear under authenticated requests?
middleBrick’s current offering scans the unauthenticated attack surface only. It will not test endpoints that require authentication unless you provide a valid session or token via the CLI or dashboard. For authenticated‑only OOB reads, you need to run the scan with appropriate headers or rely on manual code review and static analysis tools.
Is using Django’s <code>[:]</code> slicing on a QuerySet safe if I first call <code>.count()</code> to know the size?
Calling .count() does not prevent an out‑of‑bounds read; it only tells you the total number of rows. If you subsequently slice with a user‑supplied offset or limit that is negative or exceeds the count, the database will still return data outside the intended window (or raise an error). You must validate and clamp the offset/limit values before slicing, regardless of any prior count call.