Skip to content

Commit

Permalink
Add Version-based Detection for CVE-2024-51479
Browse files Browse the repository at this point in the history
  • Loading branch information
nmasdoufi-ol committed Dec 20, 2024
1 parent 368a22e commit ca1131f
Show file tree
Hide file tree
Showing 2 changed files with 214 additions and 0 deletions.
107 changes: 107 additions & 0 deletions agent/exploits/cve_2024_51479.py
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,
)
107 changes: 107 additions & 0 deletions tests/exploits/cve_2024_51479_test.py
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

0 comments on commit ca1131f

Please sign in to comment.