Skip to content

Latest commit

 

History

History
232 lines (159 loc) · 16.4 KB

File metadata and controls

232 lines (159 loc) · 16.4 KB
title Access and redaction
description Who can reach the devtools, and which values are redacted before they leave the page.
The devtools send what they read from your app to a server on your machine. Here is who can reach that server, and what is redacted on the way.

Access and redaction

The devtools read your running app and send what they find to a server on your machine. This page covers who can reach that server, and what is redacted on the way.

Don't expose the dev server beyond localhost. Values that no rule recognizes as secret are sent as they are.

At a glance

Loopback requests only. A request that sends an Origin must come from a loopback host, the Pangular Inspector extension, an entry in allowedOrigins or Vite's server.allowedHosts. Asks for a one-time code when a non-loopback host or origin is allowed. A one-time code and an origin check that accepts loopback origins and the Pangular Inspector extension. Both on by default. Binds to localhost and asks for a one-time code by default. Reaches loopback hosts out of the box. Any other host needs a click on Allow access, for that host only.

Local-only access

Vite plugin

The devtools only answer requests from this machine. When a request carries an Origin header, that origin must be a loopback host, the Pangular Inspector extension or an origin you allowed. Requests without an Origin header pass the origin check. Browsers leave the header out of some cross-site requests, such as image loads and link clicks, so the origin check alone does not stop every request from another website.

In detail, a request to the devtools must:

  • come from a loopback address (any 127.x.x.x address or ::1), and
  • have no Origin header, or an origin that is a loopback host, the Pangular Inspector extension, an entry in allowedOrigins, or a host that Vite's server.allowedHosts accepts.

Other requests get 403 with the message "Pangular Inspector only answers requests from this machine." WebSocket upgrades follow the same rules.

If you open the dev server through another hostname that points to your machine (for example myapp.test), list it in Vite's server.allowedHosts and the devtools trust it too. Add other origins with allowedOrigins:

// vite.config.ts
import analog from '@analogjs/platform';
import pangular from '@pangular-inspector/devtools/vite';
import {defineConfig} from 'vite';

export default defineConfig({
  server: {allowedHosts: ['myapp.test']},
  plugins: [analog(), pangular({allowedOrigins: ['https://tunnel.example']})],
});

One-time code

The plugin's auth option decides whether the devtools also ask for the one-time code. The server prints the code in the terminal, and a browser reads data only after it exchanges that code.

auth One-time code
not set On if server.allowedHosts or allowedOrigins allows a host other than localhost or a loopback address, otherwise off. allowedHosts: true turns it on.
true Always on.
false Always off. The loopback and origin checks still apply.

With only loopback hosts allowed, the loopback and origin checks take the place of the code.

A tunnel client runs on your machine, so the requests it forwards come from a loopback address. That is why an allowed tunnel host or origin turns the one-time code on. If your tunnel rewrites the Host header to localhost, nothing in your config names the tunnel, so pass auth: true. Don't pass auth: false while a tunnel is allowed.

Express hub

initPangularHub() has two checks, both on by default:

Check Option What it does
One-time code auth The server prints a code. A browser can read data only after it exchanges that code.
Origin check allowedOrigins Only loopback origins, the Pangular Inspector extension, or clients that send no Origin, can open the WebSocket. Pass a list to allow more origins.
// src/server.ts
import {initPangularHub} from '@pangular-inspector/devtools/hub';
import express from 'express';

const app = express();

const devtools = initPangularHub({
  allowedOrigins: ['https://tunnel.example'],
});
app.use(devtools.nodeMiddleware);

A list keeps loopback origins and the Pangular Inspector extension. See Chrome extension.

Pass auth: false only on a machine only you use. Keep it on when you allow a tunnel origin: the origin check does not tell who is on the other end of the tunnel. allowedOrigins: false turns the origin check off. Keep the check on for your own apps.

Standalone CLI

The CLI server binds to localhost and asks for a one-time code. --host changes the bind address and --no-auth turns the code off. See Standalone CLI.

MCP endpoint

The HTTP MCP endpoint answers only requests that carry a loopback Origin header. In the Vite plugin, the request must also come from a loopback address, like every devtools request.

While the one-time code is on, the endpoint also asks for a bearer token. That is the Express hub by default, and the Vite plugin when its code is on. The hub prints a generated token when it starts. Set PANGULAR_MCP_TOKEN to choose the token yourself. Requests without the right Authorization: Bearer <token> header get 401. The stdio server needs no token. See Send a token.

Without a token, the Express hub answers only requests from a loopback address. With a token, it also answers other addresses that send the right token and a loopback Origin. Any client can set that header, so treat the token like a password.

Chrome extension

The extension has host permissions for loopback hosts only: localhost and its subdomains, 127.0.0.1 and [::1], over HTTP and HTTPS. On those hosts, the panel looks for the devtools server as soon as it opens.

On any other host, the panel doesn't send a request until you click Allow access. Chrome then asks you to grant the extension that one host, on the scheme of the page and any port. The extension never asks for all hosts at once.

Granting the extension a host doesn't change what the devtools server accepts. The server still applies the checks on this page.

Every installed extension can send requests to a loopback host, with its own chrome-extension://<id> origin. So the Vite plugin and the Express hub accept one extension by default: chrome-extension://dcogniffeelebaolkkfbopmjcblhblfk. The key in the extension manifest fixes that ID, so the unpacked extension and the store build share it. Every other extension origin is refused.

If you build the extension with another key, add its origin to allowedOrigins. Each entry names one extension. The ID is on the extension card in chrome://extensions. In the Vite plugin, an extension entry does not turn the one-time code on. See Server origin.

What is redacted

Live values leave the page. They are sent to the devtools server, shown in the panel and returned to agents. Redacted values are replaced with [redacted].

Forms

A field's value is replaced with [redacted] when the field:

  • is a password field,
  • has a password, one-time-code or credit-card autocomplete,
  • sits inside .sentry-mask, .rr-mask, [data-private] or [data-pangular="mask"],
  • has a name that contains a secret word (password, token, card, cvv, apiKey and similar), or a name listed in mask, or
  • sits inside a group or array whose name contains a secret word.

The Fields view says why a field is redacted: name looks secret, password input, autocomplete is a secret kind, marked as mask, inside a secret group or listed in mask. The field details say the same where the Set editor is hidden, with a link to this section.

Those values are also removed from error messages. The form event log follows the same rules: a value that holds a JWT or bearer token, or repeats a secret from another field of the form, is shown as [redacted] in the event and in its previous value. The devtools don't write secret fields unless you unmask them (see Opt fields in or out). Other values are sent as they are, so keep real credentials out of forms you inspect.

Opt fields in or out

Mark a field in the template, or list keys on window:

<input name="nickname" data-pangular="mask" /> <input name="cardHolder" data-pangular="unmask" />
window.__PANGULAR_FORMS__ = {mask: ['iban'], unmask: ['passport']};

[data-pangular="unmask"] opts a field back in. The window setting does the same by key.

The mask and unmask lists apply to every inspector on the page, not only forms: nested keys of an object-valued control, Signal Forms fields, form writes and restores, component inputs, signals, NgRx state, pipes and the Analog load() preview all follow them. Analog server call previews are recorded on the server, so they follow redaction.secretNames and redaction.unmask only.

You can also name secret and unmasked fields on the server, with the redaction option. redaction.secretNames adds secret names for forms, the router, components, signals, NgRx, pipes, Analog and SSR & HTTP URLs, and redaction.unmask joins the window list. See Redaction options.

Unmasking also changes what the devtools can write. A key listed in unmask on window can be written. The element marker only lifts the checks that come from the element (password type, autocomplete and mask markers), so a field with a secret-looking name is still not written.

A refused write names the reason and the unmask that lifts it. The panel, form-action and fill-form show the same message.

password, passwd, passphrase, passcode, pass, pwd, secret, token, otp, totp, pin, cvv, cvc, csc, ssn, iban, card, cc, credential, credentials, cookie, authorization and jwt. Names are split on camelCase and punctuation, so userPassword and card_number both match. The pairs apiKey, privateKey, secretKey, accessKey, ccNum, ccNumber, securityCode, sessionId and sessionKey match as well. Every inspector uses this list.

Router

These are replaced with [redacted] in URLs, params, data and messages:

  • query, matrix and fragment keys that look secret (token, password, api key, code, sig, session, jwt and similar), including inside encoded return URLs,
  • JWTs, bearer tokens and long opaque tokens,
  • route params with secret-looking names.

A secret route param is known from the route config before a navigation is recognized, so link targets and a navigation that was already running when the overlay attached are masked too. A navigation that fails before that (for example inside a lazy route that failed to load) can still show it in its URL. JWTs are masked before long values are cut, so a long token never leaves a readable start behind.

A navigation whose URL was redacted cannot be replayed.

Components, signals, NgRx and pipes

Component inputs, signal values, NgRx state, and pipe inputs, outputs and async values use the same secret names and the same mask and unmask lists as forms. A value whose name looks secret is replaced with [redacted]. JWTs and bearer tokens inside strings and error messages are replaced too, NgRx strings and errors included, and so are tokens used as object keys. The URL and title of the NgRx page, and the request URL of an httpResource, are redacted like router URLs.

Analog

Server call previews and URLs are redacted: keys in JSON bodies that the forms rules treat as secret, secret query parameters (including redaction.secretNames, sig, signature and auth), key=value pairs with quoted values, JWTs and bearer tokens. This covers form action validation errors and redirect targets too. Only JSON and plain text responses get a preview, and it is cut at 1000 characters. The devtools keep the first 16 KB of a body, and a cut JSON body still has its secret-looking keys redacted. The load() data preview on the open page redacts the same keys, and JWTs and bearer tokens inside its string values. The page report's URL and hydration errors are redacted on the page, with the secret route params of the open route, and the URL, load() preview and hydration errors again on the devtools server. Keys are matched by whole words, so sessionId and apiKey are redacted while author and passengers stay visible. JSON nested deeper than the preview reads is shown as [Truncated].

SSR & HTTP

Request URLs, page URLs and error messages in the SSR & HTTP tab are redacted like router URLs, in the page and again on the devtools server. This covers SSR and client calls, and devframe_state_read.

The page title, the hydration warnings and mismatch details, and the TransferState parse error are redacted too. Response previews are redacted on the devtools server: keys that the forms rules treat as secret, JWTs, bearer tokens and secret query pairs, also in a preview that was cut inside a nested object or array. The same holds for TransferState entries: their values, keys and request URLs are redacted before they reach the panel or an agent. Large payload values are still cut, so a long value can end in [Truncated].

Checklist

Open the app on localhost. Add other hostnames or origins one by one, only when you need them. Keep auth and the origin check on in the Express hub unless the machine is yours alone. In the Vite plugin, don't pass auth: false while a tunnel host or origin is allowed. Keep real credentials out of forms and API responses you inspect. Use data-pangular="mask", window.__PANGULAR_FORMS__ or redaction.secretNames for fields the secret words miss. Set agent.readOnly or turn off actions to stop the panel and agents from writing to your app. See Configuration.

Related pages