The uWSGI stats server, enabled with --stats, serves a JSON blob containing the complete internal state of the master and all workers: PIDs, memory usage, request counts, response times, configuration details, and in-flight request data. When this socket is reachable from an untrusted network, every field in that JSON is an information-disclosure vector.
The stats server has no authentication. Access control is entirely network-level. Any client that can connect to the socket address receives the full dump with no challenge, no token, no ACL. The common pattern --stats :9191 (address with no IP specified) binds on every interface, which means the stats endpoint is open to anyone who can reach the host on that port.
What this means
The stats JSON includes far more than aggregate metrics. The exposed data includes:
- Worker PIDs (
workers[].pid): Reveals process structure and enables correlation with OS-level signals. An attacker can identify the master process for targeted signal attacks. - Memory usage (
workers[].rss,workers[].vsz): Per-worker resident and virtual memory, hinting at application size and resource limits. - File paths and configuration: Application mountpoints (
apps[].mountpoint), working directories (apps[].chdir), and other configuration details that reveal the application layout on disk. - In-flight request data:
cores[].varscan expose request headers, cookies, and URIs from requests currently being processed. If any worker is handling an authenticated request when the attacker polls, session cookies or authorization headers are in the dump.
The cores array is suppressed if uWSGI is started with --stats-no-cores, which mitigates the in-flight request data leak but does not protect the other fields.
A related but separate risk: the uWSGI protocol socket itself (the socket workers listen on, configured with --socket) is not the stats server. Exposing that port to untrusted networks allows remote code execution via the UWSGI_FILE magic variable and exec:// protocol handler. The stats server leaks information. The protocol socket enables code execution. Both are network-level access control failures, but with different severity.
flowchart LR
subgraph V["Exposure vectors"]
A["--stats :PORT\nall interfaces"]
B["UNIX socket\nmode 666"]
C["--stats-http\non public addr"]
end
subgraph L["Internal state leaked"]
D["Worker PIDs,\nRSS, VSZ"]
E["cores[].vars:\nheaders, cookies,\nURIs"]
F["File paths,\nmountpoints,\nchdir"]
end
A --> D
A --> E
A --> F
B --> D
B --> E
C --> D
C --> E
C --> FCommon causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| TCP stats bound to all interfaces | --stats :9191 with no IP specified | ss -ltnp for 0.0.0.0 or * in Local Address:Port |
| TCP stats explicitly bound to 0.0.0.0 | --stats 0.0.0.0:9191 | Same ss check. Bind shows as 0.0.0.0:9191 |
| UNIX socket with world-readable mode | --chmod-socket used without a value | ls -la /path/to/stats.sock. Mode is 666 by default |
| Container publishes the stats port | Docker -p 9191:9191 maps the port to the host | docker port <container> or ss -ltnp on the host |
| Stats enabled for uwsgitop without bind restriction | Operator added --stats for monitoring but forgot the bind address | Config file or process args for stats = without 127.0.0.1 prefix |
Quick checks
These are read-only commands. They do not change the uWSGI configuration.
# Check what address the stats server is bound to
ss -ltnp | grep uwsgi
# Check config for stats directives
grep -rn 'stats' /etc/uwsgi/ 2>/dev/null
# Read the JSON dump to see the full key structure
uwsgi --connect-and-read 127.0.0.1:9191 | jq 'keys'
# Check for in-flight request data in cores[].vars
uwsgi --connect-and-read 127.0.0.1:9191 | jq '.workers[].cores[].vars' 2>/dev/null | head -20
# If --stats-http is enabled, curl works too
curl -s http://127.0.0.1:9191/ | jq 'keys'
# For UNIX socket, check permissions
ls -la /run/uwsgi/stats.sock 2>/dev/null
# Check spooler directory permissions (task injection risk)
ls -la /var/spool/uwsgi/ 2>/dev/null
How to diagnose it
Identify the stats socket address. Check the uWSGI configuration for
statsorstats-httpdirectives. The address format tells you the bind target:127.0.0.1:PORTis localhost-only,:PORTor0.0.0.0:PORTis all interfaces, and/path/to/sockis a UNIX socket.Verify the actual bind against the kernel. Config files can be overridden by command-line arguments, environment variables, or Emperor-mode overrides. Use
ss -ltnp(TCP) orss -lxnp(UNIX) to see what the kernel actually reports. Trust the kernel’s view, not the config file.Enumerate what the JSON actually exposes. Connect to the stats socket and inspect the full key structure with
jq 'keys'. Then drill intocores[].vars(in-flight request data),apps[](mountpoints and chdir paths), andspoolers[](spooler stats). This tells you exactly what an attacker would see.Check for companion exposures. The main uWSGI protocol socket (
--socket) is a separate, higher-severity exposure. Verify that it is not bound to a public interface either. Both the stats socket and the protocol socket should be localhost or UNIX.Audit UNIX socket permissions. If using a UNIX socket for stats, verify the mode and ownership.
--chmod-socketwithout an explicit value defaults to 666, which is world-readable and world-writable.Check spooler directory writability. If the spooler is enabled, verify the directory is owned by the uWSGI user and not writable by others. The spooler directory path may be discoverable through the stats output or filesystem inspection.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
| Stats bind address | Determines the network attack surface | Anything other than 127.0.0.1 or a UNIX socket path |
| UNIX socket mode | Controls local access scope | Mode 666 or any world-readable/writable bit set |
| Spooler directory mode | Controls task injection risk | Writable by users other than the uWSGI process owner |
cores[] presence in stats output | Exposes in-flight request headers and cookies | Present in output without --stats-no-cores configured |
--stats-http enabled | Makes stats trivially scrapable with any HTTP client | Enabled on a publicly reachable port |
| Socket permission drift | Operational changes can silently widen access | Socket mode or ownership changes after a restart or package update |
Fixes
Bind the stats server to localhost
Change the config from:
stats = :9191
to:
stats = 127.0.0.1:9191
This limits connections to the local loopback interface. Monitoring tools running on the same host can still connect. Remote monitoring requires an SSH tunnel, a sidecar collector, or a proxy with authentication in front.
If you need HTTP access for tools that expect an HTTP endpoint, keep stats-http but bind to localhost:
stats = 127.0.0.1:9191
stats-http = true
--stats-http does not add authentication. It only changes the wire protocol from raw socket JSON to HTTP. The data exposed is identical either way. The risk difference is that HTTP makes the endpoint accessible to any tool that speaks HTTP, including web browsers, while the raw socket requires a client like uwsgi --connect-and-read or nc.
Use a UNIX socket with strict permissions
UNIX sockets are the safest option because filesystem permissions provide access control:
stats = /run/uwsgi/stats.sock
chmod-socket = 660
chown-socket = uwsgi:netdata
Read the stats via:
socat - UNIX-CONNECT:/run/uwsgi/stats.sock
or:
uwsgi --connect-and-read /run/uwsgi/stats.sock
Never use --chmod-socket without an explicit value. The default is 666, which defeats the purpose of using a UNIX socket.
Suppress cores to eliminate in-flight request data leaks
If your monitoring does not need per-core in-flight request data, suppress the cores array entirely:
stats-no-cores = true
This eliminates the cores[].vars exposure (headers, cookies, URIs) from the stats output. You lose the ability to diagnose stuck requests via req_info.request_start and in_request from the stats server, but you close the most sensitive data leak in the JSON.
Lock down the spooler directory
If the spooler is enabled, ensure the directory is owned by the uWSGI user and not writable by others. A writable spooler directory allows task injection: an attacker who can write files to the spool directory can queue tasks that the spooler will execute in the uWSGI process context.
# Check current state
ls -la /var/spool/uwsgi/
# WARNING: these commands change ownership and permissions on the
# spooler directory. Verify the correct user and group for your
# deployment before running.
chown uwsgi:uwsgi /var/spool/uwsgi/
chmod 700 /var/spool/uwsgi/
Verify the protocol socket is not exposed
The main uWSGI socket (--socket) is a separate, higher-severity risk than the stats socket. If exposed to untrusted networks, it allows remote code execution via the UWSGI_FILE magic variable. Verify it is bound to localhost or a UNIX socket, just like the stats socket. The official uWSGI documentation warns explicitly against exposing uwsgi protocol sockets to public networks.
Prevention
- Audit stats binding on every config change. Include a check for the stats address in your deployment pipeline. Flag any bind to
0.0.0.0, bare:PORT, or a world-readable UNIX socket. - Default to UNIX sockets for stats. Make
stats = /run/uwsgi/stats.sockwithchmod-socket = 660the standard in your base config template. This gives you filesystem-level access control instead of relying on network topology. - Monitor socket permissions for drift. File integrity monitoring (inotify, AIDE) on the stats socket file catches permission changes from operational mistakes, package updates, or restarts with different flags.
- Restrict spooler directory access. Verify the spooler directory mode and ownership as part of your security baseline. It should be mode 700, owned by the uWSGI user only.
- Use
--stats-no-coreswhen per-core data is not needed. This is the cheapest way to eliminate the most sensitive leak vector without changing the bind address. - Container awareness. If running under Docker or Kubernetes, verify that the stats port is not published to the host or exposed via a Service without network policy. Container orchestration can silently expose ports that were safe on a bare-metal deployment.
How Netdata helps
Netdata’s uWSGI collector reads the stats socket locally and derives per-second metrics from the raw JSON. For this security context, the relevant capabilities are:
- Per-worker metrics (busy ratio, RSS, harakiri count, response time, exception rate) are collected from the stats server. Netdata connecting to the stats socket validates that it remains reachable locally without requiring external exposure.
- OS-level network metrics show TCP listening sockets and connection counts at the host level. A sudden change in listening socket inventory after a config reload may indicate that the stats bind address shifted from localhost to a public interface.
- Process and file descriptor tracking at the host level provides visibility independent of the uWSGI stats server. If the stats server configuration changes, the OS-level inventory reflects the new socket.
- Correlation across dashboards lets you align config-change timestamps with metric anomalies to detect when a reload silently changes the stats bind address or socket permissions.
Related guides
- uWSGI all workers busy: reading the busy ratio before the queue fills
- uWSGI avg_rt is not a real average: why the latency number lies
- uWSGI cache subsystem: hit ratio drops and ‘full’ insert failures
- uWSGI capacity planning: the leading indicators before saturation
- uWSGI chain reload: cycling workers one at a time for zero-downtime deploys
- uWSGI cheaper subsystem: dynamic worker scaling and the false ‘missing workers’ alert
- uWSGI connection pool cascade: downstream latency that stalls every worker
- uWSGI connection refused: clients turned away when the backlog overflows
- uWSGI Emperor healthy but vassal dead: monitoring each instance independently
- uWSGI file descriptor limits: raising ulimit -n and systemd LimitNOFILE
- uWSGI in gevent/async mode: why worker busy ratio stops meaning anything
- uWSGI threaded mode and the GIL: why more threads don’t add CPU parallelism






