Skip to content

Ephemeral state and network security

dcmview is ephemeral, not automatically private. It keeps application state in memory and disappears when stopped. While it runs, every API request needs the session’s access token, and anyone who has that token and can reach the listener can retrieve sensitive DICOM content.

The viewer does not create a database, read a project configuration file, or save a reusable browser session. During one run it owns:

  • the progressive file and logical-series registry;
  • decoded display, raw-frame, and overlay caches;
  • tag-tree, semantic-context, and value-mapping caches;
  • request activity and server start time;
  • the session’s access token;
  • the current in-memory ROI annotation store and redaction boxes; and
  • in a masked session, the random keys behind its replacements.

Stopping the process releases these resources. Unexported annotation edits and all redaction boxes disappear, and the token stops working.

dcmview reads source DICOM files and an optional annotation CSV but does not modify them. Choosing Export ROIs is an explicit persistence action: the browser downloads a newly generated CSV to its configured download location.

Each dcmview process generates one access token at startup. Every request under /api must send it:

Authorization: Bearer <token>

No API path is exempt: the health check, the ROI export, and unknown API routes all require the token, over TCP and over a Unix socket. A request without a valid token gets 401 with the error code unauthorized and a WWW-Authenticate: Bearer header. The token is read from the Authorization header only; a query parameter or a cookie does not authenticate.

The viewer page and its content-hashed assets are served without the token. They contain the viewer build and no file data.

The token is 32 bytes from the operating system’s random source, written as 43 base64url characters. It is valid until the process exits, with no expiry or rotation, and a new process generates a new one.

With the token, a client can read:

  • decoded image pixels and rendered frames;
  • patient, study, series, and instance metadata;
  • local filesystem paths;
  • locally resolved DICOM relationships and value mappings;
  • in-memory annotations; and
  • viewer version, build, and scan information.

dcmview prints a launch link whose fragment is the token:

dcmview: server running at http://127.0.0.1:<port>/#token=<session-token>

A browser never sends a URL fragment to a server, so the token stays out of request lines, proxies, and Referer headers. When the page loads, the viewer takes the token out of the address bar, keeps it for that browser tab across reloads, and sends it with every API request.

Opening the viewer from the terminal, from Python view(), or in VS Code needs no extra step, because each uses the launch link. Treat that link, and the --startup-json line that wrappers read, as credentials: do not paste them into issues, chat, or shared logs.

A page opened without a valid token does not show a load error. It shows one of two pages:

  • This viewer needs its access link when the page has no token, for example a bookmarked or hand-typed http://127.0.0.1:<port>/.
  • The viewer restarted when the tab holds the token of an earlier dcmview process that used the same address.

In both cases, copy the full link, including #token=…, from the terminal where dcmview is running. Pasting it into the same tab loads the viewer.

Set DCMVIEW_TOKEN to choose the token instead of generating one, for example in a script that calls the API. The value must be non-empty and use only A-Z a-z 0-9 - . _ ~; an invalid value stops startup without echoing it. The token is never read from the command line, where other local users could see it.

--no-token turns the check off and prints a warning. Use it only for a listener behind a proxy that already authenticates: in that mode anything that can reach the listener can use the API. Setting DCMVIEW_TOKEN together with --no-token is a startup error.

  • It does not encrypt anything. Over plain HTTP on a non-loopback bind, the token and the DICOM data are readable on the network. Keep the loopback default and forward with SSH.
  • It can be visible in the browser opener’s arguments. When dcmview opens the browser itself, the operating system’s opener receives the launch link, token included, as a process argument, which other local users can read on some systems. On a machine shared with people who should not see the data, start with --no-browser and open the printed link yourself, or use --unix-socket.
  • A fixed token is not rate-limited. A token set with DCMVIEW_TOKEN is only as strong as the value you choose; use a long random one.
  • It is not per-user authorization. Anyone who holds the token and can reach the listener has the whole API.

The default --host 127.0.0.1 exposes the server only through the machine’s IPv4 loopback interface. This is the intended local and SSH-forwarded configuration.

Binding --host 0.0.0.0 or another non-loopback interface broadens who can reach the listener. dcmview warns that plain HTTP does not encrypt the access token or the DICOM data; under --no-token it warns that the endpoints are unauthenticated. Provide your own network controls if you deliberately change the host.

On Linux and macOS, --unix-socket PATH serves the viewer through a Unix domain socket instead of a TCP port, so other accounts on the same machine cannot connect to it:

  • The socket’s directory must be owned by your effective user and must not be writable by group or others. A missing directory is created with mode 0700; its own parent must already exist.
  • The socket has mode 0600, and a connection is accepted only when its peer user ID matches the server’s.
  • A lock file beside the socket, named <socket file name>.lock with mode 0600, is held while the viewer runs, so a second launch cannot replace a running viewer’s socket. The directory therefore needs a filesystem that supports file locks.
  • The socket and its lock file are removed on shutdown. A forced kill can leave a stale socket, which the next launch on that path replaces.

The token is still required over the socket. Socket mode is not available on Windows, from Python view(), or in the VS Code extension. The remote inspection guide gives the procedure.

Run dcmview beside the data, bind it to remote loopback or a private socket, and forward it to local loopback. The remote inspection guide gives the complete procedure.

At startup dcmview prints the matching ssh -L command and the link to open afterwards. Run the command on your local machine; SSH encrypts the connection, and the server stays on loopback at both ends. v0.4.1 has no built-in SSH helper.

The viewer also works behind a reverse proxy that serves it under a path prefix. The API still requires the token through the proxy, unless you start dcmview with --no-token because the proxy authenticates.

Every response carries X-Content-Type-Options: nosniff, so browsers do not reinterpret raw samples, JSON, or CSV that quote file contents as another document type. This hardens the viewer’s own pages; it is not access control.

--timeout SECONDS stops the server after the specified interval without API or browser requests, counted from the end of the scan. It is useful for unattended scripts and remote sessions, but it is not an access-control mechanism: every request resets the activity clock.

--mask replaces patient identifiers in everything the viewer displays so that files can be shown on a shared or recorded screen. Redaction boxes, drawn with the Redact tool, black out regions of the pixels. Both are display aids:

  • Display only. Source DICOM files are never modified, and no masked copy is written.
  • Nothing persisted. Pseudonyms, date offsets, hashed UIDs, and redaction boxes exist only in the memory of the running process.
  • Different on every run. Replacements are derived from random keys drawn at startup, so the same patient gets unrelated values in another session. Do not use them as research identifiers.
  • Fixed for the session. A masked session cannot be unmasked; start the viewer again without --mask.

Masking has limits that you must cover yourself:

  • Pixels are not changed. Burned-in text stays visible until you draw a redaction box over it, and the viewer cannot find such text for you.
  • Free text inside kept values, such as Study and Series Description, can contain names and dates.
  • Paths are real. The Directory view shows folder and file names as they are on disk, and the exported ROI CSV keeps real paths.
  • Anything outside the viewer page is untouched: the terminal and the editor’s own Explorer and tabs.

Masking is also not access control. A client with the token still receives real paths and every pixel you have not covered with a redaction box. Keep the loopback bind and treat screenshots and recordings of a masked session as potentially sensitive.

See Share your screen for the rules a masked session applies.

DICOM files, screenshots, exported CSVs, launch links, access tokens, paths, tags, and logs, including RUST_LOG debug output that lists skipped file paths, can contain protected health information, credentials, research identifiers, hostnames, or institutional details. Use synthetic fixtures, redacted logs, and minimal reproduction steps in public issues. Report suspected vulnerabilities privately through the project’s security policy.

dcmview is a research and development inspection tool. It is not for clinical diagnosis or clinical decision-making.