Skip to content

Security: harshithsunku/perflens

Security

SECURITY.md

Security

PerfLens puts an agent on a device and lets a server drive it. This document says what that protects and — more importantly — what it does not.

Reporting a vulnerability

Use GitHub private security advisories rather than a public issue. Include the version (perflens-agent --version, perflens version), how the agent was launched, and what an attacker would need to reach.


Threat model

What is protected. A peer must prove knowledge of a shared secret before the agent will run any command. Without it the agent will not start profiling, will not enumerate processes, will not stream device metrics, and will not update itself.

What is not protected: the channel. The wire protocol is plaintext TCP. There is no encryption, no integrity protection, and no defence against an attacker who can read or modify traffic between the server and the agent. The pairing code itself travels server → agent in the clear.

That is a deliberate trade for a tool aimed at controlled networks — lab benches, test racks, a device on your desk. If the network between you and the device is not trusted, tunnel it:

# On the device
perflens-agent --listen --bind 127.0.0.1

# On your machine
ssh -N -L 9999:127.0.0.1:9999 user@device
# then point the Live Debug wizard at 127.0.0.1:9999

Profiling data is sensitive. A profile carries function names, source file paths, stack traces and — with list_processes — the full command line of every process on the device. Command lines routinely contain secrets. Treat a saved session the way you would treat a core dump.


Authentication

The agent always holds a secret:

  • --token SECRET / PERFLENS_TOKEN, if you supply one.
  • Otherwise, in --listen mode, it generates a pairing code at startup — 16 random bytes from /dev/urandom, printed to its log — and the operator copies it into the server. There is no unauthenticated state to fall into.

The code is per agent start. Restarting the agent rotates it.

The exchange

agent → server  flag 3   {"type":"hello","version":1,"auth":"token",
                          "agent_version":"0.11.0","platform":{...}}

server → agent  flag 2   {"id":"<hex12>","cmd":"auth","args":{"token":"<code>"}}

agent  → server flag 3   {"id":"<hex12>","ok":true}
                    or   {"id":"<hex12>","ok":false,"error":"auth failed"}

The hello carries no secret. It is sent to whoever completed the TCP handshake, before that peer has proved anything, so everything in it is public by construction. Until the exchange succeeds, every command is answered {"ok":false,"error":"unauthenticated"}, and no metrics are streamed.

Three wrong codes, or 10 seconds without a valid one, and the agent drops the connection and returns to listening. While a peer has not yet authenticated, the agent keeps accepting: a new connection replaces the silent one at once, so a peer that connects and sends nothing cannot hold the slot. An authenticated session is never replaced by a newcomer.

Getting the code, in practice

The agent normally runs detached, so read it from wherever its output went:

ssh user@device 'grep -i "pairing code" /tmp/agent.log'

Prefer the environment variable over the flag

/proc/<pid>/cmdline is world-readable, and PerfLens's own list_processes reports it. A secret passed as --token is visible to every local user; one passed via PERFLENS_TOKEN is not. A generated pairing code never appears in argv at all, which is the safest of the three.

--bind

--listen binds 0.0.0.0 by default, because reaching the device from another machine is the point. Narrow it with --bind 127.0.0.1 (plus an ssh tunnel) when you want the port unreachable from the network.

Choosing the perf binary

A paired server can point the agent at a different perf with verify_perf {"perf": "/path"} — the wizard's and control bar's perf fields do exactly that — and the agent then runs that binary, with profiling arguments, until it exits. That widens what pairing grants: before it, a server chose what to profile but never which binary ran. The agent narrows it rather than trusting the path: it must be absolute (or a bare name looked up on PATH), <path> --version must print perf version, and a change is refused while collection is running. Those checks stop mistakes, not a hostile paired server — pairing is the control that matters. When the location is known in advance, prefer --perf / PERFLENS_PERF at launch.


The web UI has no authentication

perflens serve binds 127.0.0.1 by default and the UI has no login. Anyone who can reach the HTTP port can drive a connected agent and read every saved session.

Do not pass --http-bind 0.0.0.0 on a shared or untrusted network. Use an ssh tunnel to the server instead:

ssh -N -L 8080:127.0.0.1:8080 user@server

--token protects the agent connection. It does not protect the HTTP port.


Software updates

perflens-agent --update downloads a binary from the project's GitHub releases over HTTPS and replaces itself. Two things worth knowing:

  • The --version check is not integrity verification. The agent runs the downloaded binary and looks for perflens-agent in its output. That catches a wrong-architecture or truncated download. It does not catch a hostile one — by then the binary has already executed.
  • Checksums are published, and verified by the installers, not the agent. Each release asset ships a .sha256 sidecar. install-agent.sh and perflens push-agent verify it; the C agent does not, because it carries no hash implementation and will not grow one just for this. A sidecar served from the same origin as the asset stops truncation, corruption and a poisoned single object. It does not stop an attacker who controls the origin.

update is refused on a session with no configured pairing code. To update a tokenless agent, use ssh:

ssh user@device '~/.perflens/bin/perflens-agent --update'

Setting PERFLENS_UPDATE_URL to an http:// origin is refused outright.


Compatibility with pre-0.10.0 agents

Agents before 0.10.0 put their shared secret in the hello frame — which, in --listen mode, meant handing it to anyone who completed a TCP handshake. A 0.10.0 or 0.11.0 server still accepted such an agent when the token matched, with a warning. Since 0.12.0 a server with a token configured refuses it, and the rejection names the fix (perflens push-agent, or perflens-agent --update on the device). A server with no token configured sends no auth at all, so an old agent dialling it in --server mode still works; either way the hello token is stripped before the hello reaches the HTTP API.

The server also bounds what an unauthenticated peer can make it do: the hello and auth frames are capped at 64 KB, a session frame at 80 MB, and one frame's decompressed size at 256 MB; a malformed frame is dropped and logged rather than ending the session.

The web UI and cross-origin pages

The HTTP API binds to 127.0.0.1 by default and has no authentication (see --http-bind). Until 0.12.0 every response also carried Access-Control-Allow-Origin: *, which let any web page the operator visited read /api/browse (a directory listing under --browse-root), /api/agent and every saved profile from the local server. The header is gone; the UI is same-origin and the Vite dev server proxies /api.

Upgrade order: server first, then agents. A 0.10.0 agent sends no hello token, so an older server configured with --token will reject it.

Deprecation timeline

Version Change
0.10.0 Pairing-code authentication. Legacy hello tokens accepted with a warning.
0.11.0 No authentication change. Legacy hello tokens still accepted with a warning.
0.12.0 Legacy hello tokens refused when the server has a token configured.
1.0.0 A pairing code is required for both --listen and --server; only headless --output runs without one.

Refusing legacy tokens moved from 0.11.0 to 0.12.0: pre-0.10.0 agents are still downloadable from earlier releases, so they get one more release of warnings.

Supported versions

Fixes land on the latest release. There are no backports to earlier lines.

There aren't any published security advisories