quibble.fyi
Early access

Security & privacy

Written for the person who has to approve quibble. These are the claims we'll defend, and the last section lists the ones we won't make.

Where the data goes

From the browser to your application, and from your application to your issue tracker. That's the whole path. quibble is a library inside your app. There's no quibble service, and nothing is sent to us or to anyone else: no telemetry, no call home, no CDN.

Hosted feedback tools receive your users' screenshots, console logs and URLs, which makes them a data processor: a DPA, a subprocessor entry and a vendor review. quibble receives nothing, so none of that is added. Your data lives wherever your tracker does. With a self-hosted Gitea on your own hardware, it never leaves that hardware.

quibble
  1. Browser
  2. Your app
  3. Your tracker

What the token can do

quibble files every issue with one service token. It needs:

ScopeNeeded forWhat it allows
Issues: read and writeEverythingCreate, read, open and close issues; create labels
Contents: read and writeScreenshots onlyCommit to the repository

The second scope is the one to look at. GitHub gives a token no attachment endpoint, so quibble stores a screenshot by committing it to a dedicated quibble-screenshots branch. GitHub has no branch-scoped permission, so that scope covers the whole repository, and the guarantee that quibble writes nowhere else comes from its code, not from the token.

If that's not acceptable, leave screenshots off and grant Issues only. Reports still file; they just don't carry an image.

What a submitter can and can't cause

People who file are trusted but not privileged: signed in through your identity provider, holding no credential of their own.

They can create an issue with their text; tag it with labels that already exist; set a priority, which is held to P1P3; attach a screenshot they chose to take; and send page context.

They can't create a label, send a note to a repository you didn't configure, close, comment on or edit anything, go past the rate limit (when one is set and a sign-in header is present), or overwrite a signed-in identity with a typed one.

Input is capped before it reaches your tracker: context values at 500 characters, 30 fields, screenshots at 2 MB and checked to be real PNGs.

The endpoint must sit behind sign-in. It creates issues. It isn't a public form. Put it behind Cloudflare Access, your app's own sign-in or any authenticating proxy. quibble reads identity from a header it's told to trust; it doesn't sign anyone in itself.

Identity

The reporter's name comes from a trusted header set by your sign-in: Cloudflare Access by default, or any header you name, so SAML, OIDC, oauth2-proxy and mTLS front ends all work. With no proxy, quibble can ask for a name or email instead.

A trusted header always wins. A typed name is used only when no header is present, and the issue marks it self-declared. That marking is the entire guarantee: a typed identity is never verified.

Behind your sign-in- Reported by: dana@corp.com (via quibble)

Typed in, no sign-in- Reported by: dana@x.com (self-declared, via quibble)

Masking

Mark any part of a page data-quibble-mask and its text is reported as (masked) and never sent. Screenshots honour it too: marked regions are painted out with a solid fill before the image leaves the browser. A solid fill, not a blur, because a blur can be reversed.

If someone shares a different tab or screen when taking the screenshot, the masked regions wouldn't line up, so quibble refuses the screenshot rather than redact the wrong thing, and says why.

Query strings are removed from the URLs in stack traces, console output and failed-request records, because they carry tokens and session IDs far more often than anything useful.

Errors and environment details are on by default. Console output and failed network requests are off until you turn them on, because they name your own internal endpoints.

Supply chain

Installing quibble pulls in nothing: the core is pure Python standard library, and the widget is dependency-free JavaScript with no build step. The web-framework adapters are optional extras you choose.

Trackers are supported in the package itself, not through third-party plugins, so there's no ecosystem of adapters to review. For a supply-chain review, that's the whole surface: one package, no transitive dependencies, and a check that fails the build if that ever changes. This website runs the same check against the version its demo uses.

Limits we won't overstate

  • Rate limiting is per process. Several workers mean several limits. It guards against a runaway retry loop, not a determined attacker.
  • Without a sign-in header, every submitter shares one limit. The limiter never keys on a typed name, because that would be trivial to change.
  • Self-declared identity isn't verified. It's marked as such in the issue.
  • The Contents scope covers the whole repository when screenshots are on.
  • quibble doesn't authenticate anyone. It trusts a header. If anything other than your proxy can set that header, attribution means nothing.
  • A page can send anything it declares. Fields your app adds to the context are sent as your app provides them.

This website

No cookies, no analytics, and nothing stored on your device. Fonts are served from this site. When you email from a page, the subject line mentions which version of the page you saw.

The live demo downloads Python (Pyodide) from the jsDelivr CDN, so jsDelivr sees that request. After that, everything happens in your browser: the demo's tracker is an in-memory stand-in, and the page counts its network requests so you can check.

Found a security problem in quibble? Write to hello@quibble.fyi rather than raising it in public.