VYPR
Moderate severityNVD Advisory· Published Mar 24, 2026· Updated Mar 25, 2026

NiceGUI's unvalidated chunk size parameter in media routes can cause memory exhaustion

CVE-2026-33332

Description

NiceGUI is a Python-based UI framework. Prior to version 3.9.0, NiceGUI's app.add_media_file() and app.add_media_files() media routes accept a user-controlled query parameter that influences how files are read during streaming. The parameter is passed to the range-response implementation without validation, allowing an attacker to bypass chunked streaming and force the server to load entire files into memory at once. With large media files and concurrent requests, this can lead to excessive memory consumption, degraded performance, or denial of service. This issue has been patched in version 3.9.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
niceguiPyPI
< 3.9.03.9.0

Affected products

1

Patches

1
9026962b8c4f

Merge commit from fork

https://github.com/zauberzeug/niceguiEvan ChanMar 19, 2026via ghsa
2 files changed · +45 4
  • nicegui/app/range_response.py+14 4 modified
    @@ -10,9 +10,13 @@
     
     mimetypes.init()
     
    +MIN_CHUNK_SIZE = 1024
    +MAX_CHUNK_SIZE = 8192
    +
     
     def get_range_response(file: Path, request: Request, chunk_size: int) -> Response:
         """Get a Response for the given file, supporting range-requests, E-Tag and Last-Modified."""
    +    chunk_size = max(MIN_CHUNK_SIZE, min(chunk_size, MAX_CHUNK_SIZE))
         file_size = file.stat().st_size
         last_modified_time = datetime.fromtimestamp(file.stat().st_mtime, timezone.utc)
         start = 0
    @@ -29,10 +33,16 @@ def get_range_response(file: Path, request: Request, chunk_size: int) -> Respons
         range_header = request.headers.get('range')
         media_type = mimetypes.guess_type(str(file))[0] or 'application/octet-stream'
         if range_header is not None:
    -        byte1, byte2 = range_header.split('=')[1].split('-')
    -        start = int(byte1)
    -        if byte2:
    -            end = int(byte2)
    +        try:
    +            byte1, byte2 = range_header.split('=')[1].split('-')
    +            start = int(byte1)
    +            if byte2:
    +                end = int(byte2)
    +        except (IndexError, ValueError):
    +            return Response(status_code=416, headers={'Content-Range': f'bytes */{file_size}'})
    +        if start > end or start >= file_size:
    +            return Response(status_code=416, headers={'Content-Range': f'bytes */{file_size}'})
    +        end = min(end, file_size - 1)
             status_code = 206  # Partial Content
         content_length = end - start + 1
         headers.update({
    
  • tests/test_serving_files.py+31 0 modified
    @@ -73,6 +73,37 @@ def page():
         assert_video_file_streaming(url_path)
     
     
    +def test_invalid_range_header_returns_416(screen: Screen):
    +    app.add_media_files('/media', Path(TEST_DIR) / 'media')
    +
    +    @ui.page('/')
    +    def page():
    +        ui.label('Hello, world!')
    +
    +    screen.open('/')
    +    with httpx.Client() as http_client:
    +        for range_value in ['bytes=1000-500', 'bytes=9999-10000', 'bytes=abc-def', 'invalid']:
    +            r = http_client.get(f'http://localhost:{Screen.PORT}/media/test.mp4', headers={'Range': range_value})
    +            assert r.status_code == 416, f'Expected 416 for Range: {range_value}'
    +
    +
    +def test_malicious_chunk_size_is_clamped(screen: Screen):
    +    app.add_media_files('/media', Path(TEST_DIR) / 'media')
    +
    +    @ui.page('/')
    +    def page():
    +        ui.label('Hello, world!')
    +
    +    screen.open('/')
    +    with httpx.Client() as http_client:
    +        for chunk_size in [-1, 0, -9999]:
    +            r = http_client.get(
    +                f'http://localhost:{Screen.PORT}/media/test.mp4?nicegui_chunk_size={chunk_size}',
    +                headers={'Range': 'bytes=0-1000'},
    +            )
    +            assert r.status_code == 206
    +
    +
     @pytest.mark.parametrize('url_path', ['/static', '/static/'])
     def test_get_from_static_files_dir(url_path: str, screen: Screen):
         app.add_static_files(url_path, Path(TEST_DIR).parent, max_cache_age=3456)
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.