navigator.permissions.query() exposes granted/prompt/denied states across ~15 permissions — a support-and-state fingerprint leaked with no user action.
Most fingerprinting techniques need a canvas, a GPU, or an audio buffer. The Permissions API needs none of that — it was built to let a page politely check "do I have geolocation access?" before asking, but querying it across the dozen-plus permission names browsers actually support turns the answer into a small, structured fingerprint of its own, with no prompt, no click, and no pixel ever touched.
Key Takeaways
navigator.permissions.query({name})returns aPermissionStatus—granted,denied, orprompt— for a given permission, without ever showing the user a dialog.- Chromium supports roughly 15–17 queryable permission names; Firefox and Safari support far fewer, so which names resolve at all (rather than throw) is itself a browser/engine signal.
- The
granted/denied/promptstate for each permission reflects a mix of site history and OS-level settings, so the combined vector is a semi-persistent, per-origin fingerprint contribution. - The order permission names are enumerated in, and which throw
TypeErrorfor an unsupported name, differs by engine and version — a support-and-ordering fingerprint layered on top of the states themselves. - BrowserInsight's fingerprint check shows this alongside your other exposed signals so you can see how it stacks with canvas, WebGL, and fonts.
What navigator.permissions.query() actually does
The Permissions API, standardized in the W3C Permissions specification, gives scripts a way to check permission state without triggering the permission prompt that a feature like geolocation or notifications would normally show. Call it like this:
const status = await navigator.permissions.query({ name: 'geolocation' });
console.log(status.state); // "granted" | "denied" | "prompt"
No dialog appears. The browser simply reports what it already knows: whether this origin has been granted geolocation before, explicitly denied it, or would need to prompt the user if the feature were actually requested. That's the whole point of the API — it lets a site show "enable location" only when it would actually need to ask, instead of guessing.
Querying across every permission name
The interesting part, from a fingerprinting angle, is that the API accepts many different permission names, and a script can simply loop through all of them:
// Illustrative sketch — not every name below is supported in every browser
const candidateNames = [
'geolocation', 'notifications', 'push', 'midi', 'camera', 'microphone',
'background-sync', 'persistent-storage', 'ambient-light-sensor',
'accelerometer', 'gyroscope', 'magnetometer', 'screen-wake-lock',
'clipboard-read', 'clipboard-write', 'display-capture', 'nfc',
];
async function fingerprintPermissions() {
const results = {};
for (const name of candidateNames) {
try {
const status = await navigator.permissions.query({ name });
results[name] = status.state; // granted | denied | prompt
} catch {
results[name] = 'unsupported'; // name threw — browser doesn't recognize it
}
}
return results; // fed into a hash alongside other signals
}
Two independent axes come out of that loop. The first is which names resolve versus throw — that's a support fingerprint, closer to feature detection than to state, and it maps fairly directly to browser engine and version. The second is the actual state returned for each supported name — that's the part shaped by the individual user's history on that origin and by OS-level permission settings, so it varies from person to person even on identical browsers.
Why the results differ across browsers
Chromium-based browsers (Chrome, Edge, Opera, Brave) recognize the widest set — roughly 15 to 17 names depending on version, including several sensor and hardware-adjacent permissions like accelerometer, magnetometer, and ambient-light-sensor that other engines don't expose through this API at all. Firefox supports a much smaller set — geolocation, notifications, persistent-storage, and push are the reliable ones — and queries for Chromium-only names throw rather than resolve. Safari's WebKit implementation is narrower still and historically lagged both in adoption and in which names it recognizes.
That gap is not an accident of laziness; each of those sensor-adjacent permissions corresponds to a device API the engine actually implements, so navigator.permissions.query({ name: 'magnetometer' }) throwing on Firefox is a direct, reliable side effect of Firefox simply not shipping the Generic Sensor API surface that permission gates. The practical result for fingerprinting is that the supported-name set narrows down browser family and version with very little code — similar in spirit to how feature and API support differences generally leak engine identity, except here the "feature" being detected is the permission system itself.
Worth distinguishing this from a different, unrelated permission model: what an installed extension is allowed to do (read every page, access cookies, run scripts) is governed by the extension's own manifest permissions, not by navigator.permissions.query() — see Browser Extension Privacy Risks for how that separate, much broader grant works.
The state vector as entropy
Beyond which names resolve, the state each one resolves to adds a second layer. Most users have never explicitly granted or denied most permissions, so the common case is prompt across the board — a low-entropy default. But any permission a user has interacted with before — granting a site's geolocation request, blocking notifications site-wide in browser settings, granting persistent storage to a web app — flips that entry to granted or denied and stays that way until the user changes it again.
That makes the vector a hybrid signal: mostly a browser/engine fingerprint from the support pattern, with a smaller but real per-user contribution from actual permission history. Combined with other passive signals — canvas, WebGL, or media device counts — it adds another largely independent axis that a script can read for free, in a single async call, with no permission prompt of its own.
Mitigations
There's no dedicated "block permissions.query" toggle in mainstream browsers, because the API exists specifically so legitimate sites can avoid over-prompting users — removing it would make every permission-gated feature worse to use. The realistic mitigations are the same ones that constrain rendering-based fingerprints generally:
- Firefox's
privacy.resistFingerprintingstandardizes several permission-related behaviors as part of its broader uniformity approach, reducing how much the support pattern varies from the Firefox baseline. - Minimize what you grant. Every permission you actually grant or explicitly deny becomes a stable, queryable bit. Declining permissions you don't need, and revoking stale grants in your browser's site settings, keeps more of your vector at the low-entropy
promptdefault. - Use a browser with a narrower supported-name set deliberately. This is a real trade-off, not a free win — Safari and Firefox's narrower support surface also means less to fingerprint through this specific API, at the cost of some web features degrading.
- Check what you expose. Run BrowserInsight's fingerprint check to see the permission states, canvas, WebGL, and other signals your browser currently reveals, side by side.
Frequently Asked Questions
Does querying permissions require user interaction or a prompt?
No. navigator.permissions.query() reports existing state without showing any dialog. Only actually using a gated feature (like calling getCurrentPosition() for geolocation) triggers a prompt, and only when the state is prompt rather than already granted or denied.
Can this technique identify me on its own?
Not reliably by itself. Most permissions sit at the prompt default for most users, so the state vector alone has limited entropy. It's most useful to trackers combined with the browser-support pattern (which correlates with engine/version) and other passive signals like canvas or fonts.
Why do some permission names throw errors instead of returning a state?
Because the browser doesn't implement the underlying feature at all. navigator.permissions.query({ name: 'magnetometer' }) throws on a browser with no Generic Sensor API support, since there's no permission system to check state for a feature that doesn't exist in that engine.
Does resistFingerprinting stop this?
Firefox's privacy.resistFingerprinting reduces some of the variance in permission-related behavior as part of its general uniformity strategy, but the underlying API and its state-reporting behavior remain — it narrows the signal rather than eliminating it, the same way it does for canvas and layout APIs.
Conclusion
The Permissions API is a small, sensible convenience — check before you prompt — that happens to leak two independent signals for free: which permission names a browser recognizes at all, and what state each one currently holds. Neither is loud on its own, but stacked with the dozens of other passive signals a page can read without asking, it's one more reason "fingerprinting" means far more than canvas and WebGL.
Recommended Reading:


