generated from Ostorlab/template_agent
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Version-based Detection for CVE-2024-51479
- Loading branch information
1 parent
368a22e
commit ca1131f
Showing
2 changed files
with
214 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
"""Agent Asteroid implementation for CVE-2024-51479""" | ||
|
||
import re | ||
import datetime | ||
|
||
import requests | ||
from packaging import version | ||
|
||
from agent import definitions | ||
from agent import exploits_registry | ||
from agent.exploits import webexploit | ||
|
||
DEFAULT_TIMEOUT = datetime.timedelta(seconds=90) | ||
|
||
VULNERABILITY_TITLE = "Next.js Authorization Bypass via Middleware" | ||
VULNERABILITY_REFERENCE = "CVE-2024-51479" | ||
VULNERABILITY_DESCRIPTION = ( | ||
"A recently disclosed security vulnerability in Next.js, a popular React framework used by millions of developers worldwide, " | ||
"could have allowed unauthorized access to sensitive application data. The vulnerability stemmed from an authorization bypass issue " | ||
"in Next.js middleware when performing authorization checks based on pathname. This affected versions 9.5.5 through 14.2.14 and allowed " | ||
"unauthorized access to pages under the root directory of an application." | ||
) | ||
RISK_RATING = "HIGH" | ||
|
||
CHUNK_FILE_PATTERN = re.compile( | ||
r'<script src="(/_next/static/chunks/main-[a-zA-Z0-9-]+\.js)" defer=""></script>' | ||
) | ||
VERSION_PATTERN = re.compile(r't\.version="(\d+\.\d+(?:\.\d+)?)"') | ||
MAX_VULNERABLE_VERSION = version.parse("14.2.14") | ||
|
||
|
||
def _fetch_chunk_file(target_url: str, chunk_path: str) -> str | None: | ||
""" | ||
Fetches the JavaScript chunk file to extract the version information. | ||
Args: | ||
target_url: The base URL of the target application. | ||
chunk_path: The path to the chunk file. | ||
Returns: | ||
The content of the chunk file, if accessible, or an empty string. | ||
""" | ||
try: | ||
full_url = f"{target_url}{chunk_path}" | ||
response = requests.get(full_url, timeout=DEFAULT_TIMEOUT.seconds, verify=False) | ||
if response.status_code == 200: | ||
return response.text | ||
except requests.RequestException: | ||
pass | ||
|
||
return None | ||
|
||
|
||
def _extract_version(chunk_content: str) -> str | None: | ||
""" | ||
Extracts the Next.js version from the chunk file content. | ||
Args: | ||
chunk_content: The content of the fetched chunk file. | ||
Returns: | ||
The extracted version string, if found, or None. | ||
""" | ||
match = VERSION_PATTERN.search(chunk_content) | ||
if match is not None: | ||
return match.group(1) | ||
return None | ||
|
||
|
||
@exploits_registry.register | ||
class CVE202451479Exploit(webexploit.WebExploit): | ||
accept_request = definitions.Request(method="GET", path="/") | ||
accept_pattern = [re.compile(r"/_next/static")] | ||
|
||
def check(self, target: definitions.Target) -> list[definitions.Vulnerability]: | ||
vulnerabilities: list[definitions.Vulnerability] = [] | ||
|
||
root_response = requests.get( | ||
target.origin, timeout=DEFAULT_TIMEOUT.seconds, verify=False | ||
) | ||
if root_response.status_code != 200: | ||
return vulnerabilities | ||
|
||
chunk_match = CHUNK_FILE_PATTERN.search(root_response.text) | ||
if chunk_match is None: | ||
return vulnerabilities | ||
|
||
chunk_path = chunk_match.group(1) | ||
chunk_content = _fetch_chunk_file(target.origin, chunk_path) | ||
|
||
if chunk_content is not None: | ||
extracted_version = _extract_version(chunk_content) | ||
if ( | ||
extracted_version is not None | ||
and version.parse(extracted_version) <= MAX_VULNERABLE_VERSION | ||
): | ||
vulnerability = self._create_vulnerability(target) | ||
vulnerabilities.append(vulnerability) | ||
|
||
return vulnerabilities | ||
|
||
metadata = definitions.VulnerabilityMetadata( | ||
title=VULNERABILITY_TITLE, | ||
description=VULNERABILITY_DESCRIPTION, | ||
reference=VULNERABILITY_REFERENCE, | ||
risk_rating=RISK_RATING, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
"""Unit tests for Agent Asteroid: CVE-2024-51479""" | ||
|
||
import requests_mock as req_mock | ||
from agent import definitions | ||
from agent.exploits import cve_2024_51479 | ||
|
||
|
||
def testCVE202451479_whenVulnerable_reportFinding( | ||
requests_mock: req_mock.Mocker, | ||
) -> None: | ||
"""CVE-2024-51479 unit test: case when target is vulnerable.""" | ||
exploit_instance = cve_2024_51479.CVE202451479Exploit() | ||
|
||
requests_mock.get( | ||
"http://localhost:80/", | ||
text='<script src="/_next/static/chunks/main-e9671ac36ff4266e.js" defer=""></script>', | ||
status_code=200, | ||
) | ||
|
||
requests_mock.get( | ||
"http://localhost:80/_next/static/chunks/main-e9671ac36ff4266e.js", | ||
text='t.version="14.2.14"', | ||
status_code=200, | ||
) | ||
|
||
target = definitions.Target("http", "localhost", 80) | ||
|
||
accept = exploit_instance.accept(target) | ||
vulnerabilities = exploit_instance.check(target) | ||
|
||
assert accept is True | ||
assert len(vulnerabilities) > 0 | ||
vulnerability = vulnerabilities[0] | ||
assert vulnerability.entry.title == "Next.js Authorization Bypass via Middleware" | ||
assert ( | ||
vulnerability.technical_detail | ||
== "http://localhost:80 is vulnerable to CVE-2024-51479, Next.js Authorization Bypass via Middleware" | ||
) | ||
|
||
|
||
def testCVE202451479_whenSafe_reportNothing(requests_mock: req_mock.Mocker) -> None: | ||
"""CVE-2024-51479 unit test: case when target is safe.""" | ||
exploit_instance = cve_2024_51479.CVE202451479Exploit() | ||
|
||
requests_mock.get( | ||
"http://localhost:80/", | ||
text='<script src="/_next/static/chunks/main-e9671ac36ff4266e.js" defer=""></script>', | ||
status_code=200, | ||
) | ||
|
||
requests_mock.get( | ||
"http://localhost:80/_next/static/chunks/main-e9671ac36ff4266e.js", | ||
text='t.version="14.2.15"', | ||
status_code=200, | ||
) | ||
|
||
target = definitions.Target("http", "localhost", 80) | ||
|
||
accept = exploit_instance.accept(target) | ||
vulnerabilities = exploit_instance.check(target) | ||
|
||
assert accept is True | ||
assert len(vulnerabilities) == 0 | ||
|
||
|
||
def testCVE202451479_whenChunkNotFound_reportNothing( | ||
requests_mock: req_mock.Mocker, | ||
) -> None: | ||
"""CVE-2024-51479 unit test: case when chunk file is not found.""" | ||
requests_mock.get( | ||
"http://localhost:80/", | ||
text="<html><body>No chunk here</body></html>", | ||
status_code=200, | ||
) | ||
|
||
exploit_instance = cve_2024_51479.CVE202451479Exploit() | ||
target = definitions.Target("http", "localhost", 80) | ||
|
||
accept = exploit_instance.accept(target) | ||
|
||
assert accept is False | ||
|
||
|
||
def testCVE202451479_whenChunkFileNotAccessible_reportNothing( | ||
requests_mock: req_mock.Mocker, | ||
) -> None: | ||
"""CVE-2024-51479 unit test: case when chunk file is inaccessible.""" | ||
|
||
requests_mock.get( | ||
"http://localhost:80/", | ||
text='<script src="/_next/static/chunks/main-e9671ac36ff4266e.js" defer=""></script>', | ||
status_code=200, | ||
) | ||
|
||
requests_mock.get( | ||
"http://localhost:80/_next/static/chunks/main-e9671ac36ff4266e.js", | ||
status_code=404, | ||
) | ||
|
||
exploit_instance = cve_2024_51479.CVE202451479Exploit() | ||
target = definitions.Target("http", "localhost", 80) | ||
|
||
accept = exploit_instance.accept(target) | ||
vulnerabilities = exploit_instance.check(target) | ||
|
||
assert accept is True | ||
assert len(vulnerabilities) == 0 |