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.
- Browser
- Your app
- Your tracker
What the token can do
quibble files every issue with one service token. It needs:
| Scope | Needed for | What it allows |
|---|---|---|
| Issues: read and write | Everything | Create, read, open and close issues; create labels |
| Contents: read and write | Screenshots, on GitHub only | Commit to the repository |
The second scope is the one to look at, and only on GitHub. Gitea,
Forgejo and GitLab attach a screenshot to the issue itself, so they need nothing beyond
issue access. GitHub gives a token no attachment endpoint, so there 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.
On GitHub, 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 P1–P3; 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: [email protected] (via quibble)
Typed in, no sign-in- Reported by: [email protected] (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.
- On GitHub, 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 and no analytics. The only thing kept on your device is your light or dark choice, in your browser's local storage, and only if you use the switch. 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 runs Python (Pyodide) in your browser, served from this site along with everything else, so no third party sees your visit. The demo's tracker is an in-memory stand-in, and the page counts its network requests so you can check. Third-party code it uses is listed, with licences, in /vendor/LICENSES.txt.
Found a security problem in quibble? Write to [email protected] rather than raising it in public.