FortiSIEM gRPC Path Traversal RCE with a Public PoC (CVE-2026-106173)
FortiSIEM 7.5.0's unauthenticated gRPC service on TCP 27918 lets an attacker overwrite files for RCE; fixed in 7.5.1, public PoC, no known exploitation.
TL;DR
- CVE-2026-106173 is a path traversal plus null byte injection in a FortiSIEM gRPC service that lets an unauthenticated attacker write arbitrary content to any path on the appliance.
- Overwriting a health-check script that cron runs every 60 seconds turns that file write into remote code execution.
- Severity: Critical, CVSS v3.1 9.4, as rated by Fortinet.
- Only FortiSIEM 7.5.0 is affected. FortiSIEM 7.6 and FortiSIEM Cloud are not affected, and no other release is listed.
- Fortinet discovered the flaw internally (Gwendal Guégniaud of the Fortinet Product Security team) and published advisory FG-IR-26-180 on October 13, 2026, with no known exploitation.
- Horizon3.ai published a technical deep-dive with a working proof of concept on October 20, 2026.
- CISA's record marks a public proof of concept with no known exploitation.
- Upgrade FortiSIEM 7.5.0 to 7.5.1 or above now, and until then restrict access to TCP port 27918. That blocks exploitation by cutting unauthenticated access to the vulnerable service, but it does not remove the flaw.
Am I affected?
You are affected if both of these hold:
- You run FortiSIEM 7.5.0. Fortinet's advisory lists no other affected release: 7.6 is not affected, and FortiSIEM Cloud is not affected.
- The gRPC service in question is reachable from an untrusted network position. phQueryMaster is the gRPC service that listens on TCP 27918 on every interface of the appliance, and its TLS configuration does not request client certificates, so any network position that can reach the port is an attacker position.
| Software | Affected versions | Fixed version |
|---|---|---|
| FortiSIEM 7.5.x (self-managed) | 7.5.0 | 7.5.1 or above |
| FortiSIEM 7.6.x | not affected | not applicable |
| FortiSIEM Cloud | not affected | not applicable |
Check the exposure that matters, which is the port, from the networks an attacker could use:
nmap -p 27918 --open <fortisiem-host> # phQueryMaster's gRPC service; a "27918/tcp open" line means reachable, no output means filtered or closed, which is the safe state
FortiSIEM 7.5.0 is the only affected release, so the version check is one question: confirm the build in the FortiSIEM admin interface, and if it is 7.5.0, upgrade before doing anything else. Segmentation does not replace the patch. A well-segmented internal SIEM still runs the vulnerable service, and a single compromised host on that segment can reach it. Upgrade regardless of today's reachability, because the fix removes the flaw itself.
How do I fix it?
Upgrade FortiSIEM 7.5.0 to 7.5.1 or above, following Fortinet's FortiSIEM upgrade guide, and restart the appliance services.
Until the upgrade lands, restrict access to TCP port 27918. Fortinet's advisory gives this as the workaround ("Restricting access on tcp port 27918 prevents the issue"): it blocks exploitation by cutting unauthenticated access to the service, but the flaw stays in place.
The service binds every interface on the appliance, so enforce the restriction in the surrounding firewall or network segmentation, not on the appliance. It is a network block, not a patch: any path that still reaches port 27918, including a compromised host on the same segment, keeps the flaw reachable.
After the upgrade, inspect /opt/charting/redishb.sh and /opt/phoenix/bin/phLicenseTool, the two files Horizon3 flags as realistic overwrite targets, because an overwrite that happened before the upgrade survives it.
What is the root cause?
The gRPC SendQueryResult handler builds a file path by concatenating an attacker-controlled header into it, and copies header values with the client-declared length, so a null byte inside the header truncates the path to any exact file name the attacker chooses.
The phQueryMaster service starts a gRPC server on TCP 27918; Horizon3 quotes its startup code from libphGrpcServices.so:
// grpcNotificationServer::run()
unsigned short port = mServerPort; // command_port = 7918
std::string addr = "[::]:" + phSecureUtils::getGrpcPort(port); // [::]:27918
grpc::SslServerCredentialsOptions sslOps;
// sslOps.client_certificate_request stays 0 == GRPC_SSL_DONT_REQUEST_CLIENT_CERTIFICATE
auto creds = grpc::SslServerCredentials(sslOps);
builder.AddListeningPort(addr, creds, 0);
phSecureUtils::getGrpcPort() maps the appliance's base command port (7918) to the gRPC service's listening port 27918, which is why the quoted code starts from 7918 but binds [::]:27918. The address binds every interface, and the client-certificate option left at zero means any client can connect without a certificate and without authentication. The vulnerable RPC and its handler, quoted from grpcNotificationService.cpp:
replaceString(srcIp, ".", "_");
std::string fileDir = "/query/result/" + queryId; // concatenation, no validation
if (!phMiscUtils::DirExists(fileDir.c_str()))
phMiscUtils::mkdir(fileDir.c_str());
std::stringstream path;
path << fileDir << "/result-" << srcIp << "-"
<< periodStart << "-" << periodEnd << "-"
<< mGetQueryResultUniqId() << ".dat";
std::ofstream output;
output.open(path.str().c_str(), ios::binary|ios::out|ios::trunc);
while (reader->Read(&req))
output.write(req.file_content().data(), req.file_content().size());
Traversal: the query-id header from the gRPC message is concatenated into the directory path with no validation, so ../../../../../tmp/x walks out of the query directory and gives the attacker arbitrary directory control. On its own the file name still embeds parts of the request, so the write lands in a chosen directory under a server-appended result-...dat name.
Null byte truncation: the server copies the header values using the exact length the client declares, so a query-id of ../../../../../opt/charting/redishb.sh\0ignored carries an embedded null byte into the path. The operating system treats the null byte as the end of the string, discards the server's own result-...dat suffix, and the write lands on the exact path that preceded it.
How is it exploited?
Exploitation is one unauthenticated SendQueryResult gRPC call: a query-id with ../../ traversal and a null byte makes the streamed file_content overwrite /opt/charting/redishb.sh, and cron runs it within 60 seconds.
In Horizon3's published proof of concept, that request is assembled by hand. It connects to TCP 27918 with no authentication and no client certificate, calls SendQueryResult with the metadata query-id: ../../../../../opt/charting/redishb.sh\0ignored, src-ip: 127.0.0.1, period-start: 0 and period-end: 0, and streams the payload as file_content.
The server then does the rest. The handler builds the directory from the traversal, outside /query/result. The null byte truncates the constructed path, so the streamed bytes overwrite /opt/charting/redishb.sh in place, keeping the file's original owner and permissions.
redishb.sh is a health-check script for the Redis service that a cron job executes every 60 seconds, so within a minute of the overwrite the appliance runs the attacker's code.
Overwriting an existing file takes one request; creating a brand-new file at an exact name takes two, because the first request has to establish the destination directory before the second writes the exact name through the null byte truncation. The request is assembled by hand because curl and standard gRPC libraries restrict null bytes in header values, so the flaw is exploitable but not trivially scriptable.
How does the attack flow?
One unauthenticated gRPC call overwrites a script that cron runs every minute, so the delay between file write and code execution is at most 60 seconds.
reaches tcp/27918"] --> B["SendQueryResult with query-id
../../../../../opt/charting/redishb.sh
+ null byte"] B --> C["Directory built by
concatenation, no validation"] C --> D["Null byte truncates the path
to the exact file name"] D --> E["Streamed bytes overwrite
/opt/charting/redishb.sh"] E --> F["Cron runs the script
within 60 seconds"]
What can an attacker do?
An attacker with network access to TCP 27918 on a FortiSIEM 7.5.0 appliance can:
- Write arbitrary content to an arbitrary path on the appliance, in a single request when overwriting an existing file.
- Execute that content with the privileges of the overwritten script's execution context within 60 seconds, by targeting any script or binary the appliance runs on a schedule, demonstrated by Horizon3 through the Redis health-check script
/opt/charting/redishb.sh. - Use the same primitive to corrupt configuration, tamper with the SIEM's own binaries (Horizon3 lists
/opt/phoenix/bin/phLicenseToolas a second file of interest), or plant persistence.
No authentication, no user interaction, and no privileges are required, only network reach to the port. The practical limit is reachability: Fortinet's workaround is a network restriction precisely because any path that still reaches 27918, including a compromised host on the same segment, keeps the flaw exploitable.
What does the fix have to close?
Fortinet fixed the flaw in FortiSIEM 7.5.1; the advisory does not publish the patch diff, so the definitive statement of the fix is the version guidance itself.
What the fix has to close follows from Horizon3's analysis of the two halves of the bug: the query-id must stop reaching the directory path unvalidated, so traversal sequences can no longer walk out of /query/result, and the copied header values must stop being able to inject a null byte into the constructed path. The workaround addresses neither; it removes the network path to the vulnerable handler instead.
How do I check if I was compromised?
Horizon3 states plainly that no application log explicitly reveals this exploitation, so the check is file forensics. From the deep-dive's indicators:
- Run
stat /opt/charting/redishb.shand compare its modification (mtime) and change (ctime) timestamps against the surrounding files; an overwrite whose timestamps diverge from the surrounding files is the signal to investigate. - Do the same for
/opt/phoenix/bin/phLicenseTool, the second file Horizon3 flags as a target. - Review cron and process activity on the appliance for unexpected child processes, and treat any shell or interpreter spawned by a scheduled script, or any outbound connection the appliance initiates on its own, as related until proven otherwise.
Fortinet's advisory records no known exploitation, and CISA's record marks a public proof of concept with no known exploitation, so at this writing there is no campaign indicator to hunt for. The timestamp comparison and the process review above are the checks available.
Timeline
- 2026-10-13: Fortinet publishes advisory FG-IR-26-180 with the CVE-2026-106173 record, rating it Critical, CVSS v3.1 9.4, unauthenticated, with no known exploitation.
- 2026-10-20: Horizon3.ai publishes the deep-dive and a working proof of concept, tracing the full chain from the gRPC port to the 60-second cron execution.
- 2026-10-26: this post. No exploitation in the wild is recorded by Fortinet or CISA.
FAQ
Is port 27918 exposed by default?
Not by FortiSIEM's own configuration, but yes by default binding: the phQueryMaster service listens on every interface. Actual reachability depends on the surrounding network; an appliance reachable only from a management network keeps the port off the attacker's path. Confirm from the networks that matter with nmap -p 27918 --open <fortisiem-host>: a 27918/tcp open line means reachable, and no port line means filtered or closed as intended.
Does the TLS configuration stop an attacker?
No. The service presents TLS credentials but leaves the client-certificate option at "do not request", so any client can complete a connection and call the RPC without a certificate. The null byte rides inside a raw HTTP/2 header value, below the layer a TLS stack inspects, so a stock TLS stack does not reject it.
Why does the null byte give full file-name control?
Because the server copies header values using the exact length the client declares, a null byte inside the header survives into the path string, and the operating system stops reading the path at that null byte. That discards the server's own result-<src>-<start>-<end>-<id>.dat suffix, which would otherwise fix the file name. The result is an exact-path write; the only extra step is assembling the message by hand.
Is FortiSIEM 7.6 or FortiSIEM Cloud affected?
No. Fortinet's advisory lists FortiSIEM 7.6 and FortiSIEM Cloud as not affected and lists 7.5.0 as the only affected release. Deployments on 7.6 need no action for this CVE; deployments on 7.5.0 need the 7.5.1 upgrade.
Could an attacker use the write for something other than code execution?
Yes. Arbitrary file write is a general primitive: corrupting configuration files, replacing binaries that run at boot, or planting credentials all work, and Horizon3's analysis lists several cron-driven services on the appliance that ingest files. Code execution through the 60-second health-check script is simply the shortest demonstrated path.
Has anyone been hit?
Fortinet's advisory says known exploited: no, and CISA's record for the CVE marks a public proof of concept with no known exploitation. Horizon3's proof of concept is public, so the gap between a public proof of concept and in-the-wild exploitation is short, which is the reason the upgrade should not wait.
References
- Fortinet PSIRT advisory FG-IR-26-180, published October 13, 2026, with the affected-release table, the TCP 27918 workaround and the internal-discovery credit: https://www.fortiguard.com/psirt/FG-IR-26-180 (checked October 26, 2026)
- CVE-2026-106173 record, Fortinet-rated CVSS v3.1 9.4 Critical, unauthenticated, CISA ADP marking a public proof of concept: https://cveawg.mitre.org/api/cve/CVE-2026-106173 (checked October 26, 2026)
- Horizon3.ai deep-dive by Zach Hanley, October 20, 2026, quoting the gRPC startup and handler code, the null-byte mechanism, the redishb.sh cron target and the timestamp-based indicators: https://horizon3.ai/attack-research/cve-2026-106173-deep-dive-fortisiem-grpc-path-traversal-file-write-rce/ (checked October 26, 2026)
- Horizon3's proof of concept: https://github.com/horizon3ai/CVE-2026-106173 (checked October 26, 2026)
- Horizon3's prior FortiSIEM research on the phMonitor service, CVE-2023-34992, CVE-2024-23108 and CVE-2025-64155: https://horizon3.ai/attack-research/disclosures/cve-2025-64155-three-years-of-remotely-rooting-the-fortinet-fortisiem/ (checked October 26, 2026)
- The Hacker News on the public PoC, October 2026: https://thehackernews.com/2026/10/critical-fortisiem-flaw-gets-public.html (checked October 26, 2026)