Skip to content

Security: gowtham012/PinPoint

Security

SECURITY.md

Security

Reporting

Please report vulnerabilities privately through GitHub's Report a vulnerability button on the Security tab, rather than opening a public issue.

For what is collected, where it is stored and who else can see it, see PRIVACY.md.

Design notes

Pinpoint moves text and screenshots from your browser to your coding agent, so the trust boundaries matter.

  • The bridge listens on 127.0.0.1 only. It also refuses any request carrying a web page's Origin header — only chrome-extension:// callers, origin-less local CLI tools and the bridge's own origin (the /ios and /android picker page it serves, sent with X-Frame-Options: DENY and frame-ancestors 'none' so no site can frame it) are accepted, so a site you happen to be visiting cannot read your notes or plant instructions for your agent. DNS rebinding does not help an attacker either: the Host header must be loopback.
  • Only Pinpoint's own extension gets in. Any installed extension can call 127.0.0.1, so an extension origin alone proves nothing. The bridge also requires a token it keeps in ~/.pinpoint/token (created on first start, mode 600). Chrome and Firefox hand it to the extension through the native host, which only Pinpoint's extension id may reach (on Firefox, the native-messaging manifest's allowed_extensions names only the gecko id pinpoint@gowtham012.github.io, and the host still accepts only an integer port); in Safari, or a browser without the native host, you paste it into the popup once (node bridge/cli.js token). /health still answers without it, but only to say who it is.
  • A web page cannot drive Pinpoint. The in-page UI is a closed shadow root, so page scripts cannot reach the comment box or the Send button, and the picker ignores synthetic (untrusted) events. Endpoints with side effects that a plain GET could reach (/annotations/wait, /ios/screen, /android/screen, which runs adb) need an x-pinpoint: 1 header, which an <img> or a link cannot send. Dictation starts only from a trusted click (event.isTrusted) inside the closed shadow root, so neither page scripts nor synthetic events can open the microphone.
  • The bridge identifies itself with a service: "pinpoint-bridge" marker, so the extension will not mistake some other server on port 7331 for it and start posting your page content there.
  • Everything scraped from a page is labelled untrusted in the markdown handed to an agent. Only the comment you typed is presented as an instruction; element text, HTML, attributes and styles are explicitly framed as data for locating the element, flattened onto one line each (so page text cannot start a fake **Change:** line), and the HTML sits in a fence longer than any backtick run inside it. Android view-tree text is untrusted app data too: the picker renders it with textContent only, and pending.md flattens and labels it.
  • Page context is untrusted and redacted. Error messages, stack lines and failed-request URLs come from the page, like the element's text and HTML. The service worker checks their shape, keeps only the last 60 seconds and redacts them (extension/redact.js) before they reach the bridge; if the redaction script is missing, nothing is attached. Before page errors and failed requests leave the browser, they are cut to 2000 characters and then scrubbed. Query strings and fragments are dropped from URLs, but a stack frame's line:col is kept. Passwords in URLs (scheme://user:pass@host) are removed. Bearer and Basic credentials, JWTs, long hex and base64 strings are replaced. So are the values of key=value and "key":"value" pairs whose name ends in a secret word (api key, token, secret, password/passwd/pwd, session id, sid, signature/sig, auth), including prefixed names such as DB_PASSWORD or csrf_token. Each string is capped at 300 characters. In the agent's markdown every entry is flattened to one line with emphasis and backticks removed, under a label saying it is page data, not instructions.
  • Recorded steps are scraped page data. They appear in the markdown below the "untrusted data" line, flattened (no newlines, emphasis or backticks), and the store caps them at 10. Only trusted pointerdown events in the top frame become steps, and only the top frame may start, add to or stop a recording.
  • New routes keep the same guards. POST /annotations/:id/accept and POST /annotations/:id/after sit behind the same Host, Origin and x-pinpoint-token checks as every other annotation route. /after only re-checks a resolved note's element in a tab already showing that page, and only while the extension has been heard from in the last two minutes. /android and /android/annotations accept only the bridge's own origin or a token-carrying extension, like the /ios routes.
  • The source-map lookup never leaves the page's origin. When a production build has no framework debug info, the service worker (extension/sourcemap.js) fetches the scripts the page loaded and their source maps to resolve the source hint. It runs only for a local development page, takes the origin from the browser's own record of the asking frame (never from the message), and fetches only URLs on that origin — or another loopback port when the page is on loopback — with redirects refused, no cookies, an 8 MB cap per file and a 4 s budget. A map on a CDN or any third-party host is not requested. Running in the worker rather than the page means the page's CSP and any fetch it has wrapped play no part, and the page cannot see or answer the requests. Only the resulting file, line and component name go into the note.
  • The extension only injects itself into local development pages — localhost, the loopback range, private LAN addresses, .local/.test/.localhost hosts and file://. Anywhere else it is not present unless you turn it on for that tab from the toolbar.
  • The <all_urls> host permission is required by Chrome for tabs.captureVisibleTab (the element screenshot) and nothing else; a narrower permission does not grant that API.
  • Screenshots never become loose image files. They are stored base64-encoded inside ~/.pinpoint/annotations.json, and a queued capture is abandoned rather than taken if the page changed first — you never get a screenshot of the wrong page.

Release credentials

Store credentials are repository secrets only: EDGE_API_KEY / EDGE_CLIENT_ID / EDGE_PRODUCT_ID (Edge Add-ons; API keys expire after 72 days) and AMO_JWT_ISSUER / AMO_JWT_SECRET (addons.mozilla.org). tools/edge-publish.sh hands the Edge key to curl through a temporary header file, so it is never on a command line or in the log.

There aren't any published security advisories