# Local Font Access API permission: what it guards

> The Local Font Access API permission, local-fonts, guards queryLocalFonts(). Measuring text needs no permission, so the gate binds only polite pages.

Source: https://mosaicbrowser.com/blog/querylocalfonts-permission

## queryLocalFonts and the permission that guards your font list

The Local Font Access API permission, local-fonts, guards queryLocalFonts(). Measuring text needs no permission, so the gate binds only polite pages.

![A prompt on the front door, and the side door open](https://mosaicbrowser.com/covers/querylocalfonts-permission.webp)

The Local Font Access API permission is called `local-fonts`, and it keeps your font list behind a prompt. A page calls `window.queryLocalFonts()`, the browser asks you, and only a yes returns the fonts. The older trick, measuring text in each font a script wants to test, needs no permission at all. So the prompt changes what a polite page has to do far more than what a tracker can learn.

## What queryLocalFonts() hands over

The call returns a promise of `FontData` objects, one per installed font face. Each carries four names: `postscriptName`, `fullName`, `family` and `style`. Each also has a `blob()` method that returns the font file's raw bytes, the SFNT data a design tool needs to render or edit it. A page that only wants a few faces can pass `{ postscriptNames: [...] }` and get just those back.

That is a different kind of answer from what fingerprinting scripts usually collect. A measuring script can only test names it already has on a list, so a font you installed from a small foundry, or one that came with some audio software, goes unseen unless the script happens to guess it. The API has no list to guess from. It returns everything the operating system reports, including the faces nobody would think to test.

## The three gates on the Local Font Access API

The [WICG specification](https://wicg.github.io/local-font-access/) and Chromium put three checks in front of that list, and a page has to pass all of them.

1. A user gesture. The call has to come from a real click or key press. A script that runs on page load and calls it gets nothing.
2. The `local-fonts` permission. The browser asks the person. Chrome remembers the answer for that site, so the next visit does not ask again.
3. Permissions-Policy. The `local-fonts` directive defaults to `self`. A cross-origin iframe, such as an embedded ad or a third-party widget, cannot call it unless the page that embeds it grants access with `allow="local-fonts"`. Where the policy blocks it, the promise rejects with a `SecurityError`.

Each gate makes sense for the use the API was written for: a web design tool that wants your real fonts in its menu. You click "Use my fonts", you see the prompt, you say yes.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: Machines that exist](https://mosaicbrowser.com/features/real-fingerprints)

## The bit a site can read without asking

One thing leaks without any prompt. `navigator.permissions.query({ name: 'local-fonts' })` returns `granted`, `prompt` or `denied`, and calling it shows nothing to the user. It only reports the decision for the site that asks, so it says nothing about what you told anyone else. A fresh browser answers `prompt`.

That default matters more than it looks. We learnt it on a different permission: `Notification.permission` read `denied` on all 344 profiles on our build Mac, because one content setting was forced for every launch, while an unconfigured Chrome says `default`. Nothing about the host leaked out. One value was pinned across every profile, and a site reads that as well as a leak ([the notification story](https://mosaicbrowser.com/blog/notification-permission-denied) has the details). So Mosaic leaves `local-fonts` exactly where stock Chrome leaves it, and a new profile answers `prompt`.

## Why the prompt does not stop font fingerprinting

A tracker never needs the prompt. It draws a test string in a generic family, records the width and height, then draws it again with the candidate font first and the generic family as fallback. A different size means the font is installed. It runs in a hidden element, on page load, in every browser, and asks nobody. [How websites list your fonts without a font API](https://mosaicbrowser.com/blog/how-sites-detect-installed-fonts) walks through the script line by line.

So the permission blocks the honest route and leaves the quiet one open. A script that calls `queryLocalFonts()` is announcing itself with a dialog, which is the last thing a tracker wants. What the gate really protects is the long tail: the faces that are not on any candidate list, and the font files themselves.

## What a profile's fonts are in Mosaic

In Tesseract Chrome, the font list a page finds is this computer's own. That was not always so. Our engine used to hide about 2% of font families per profile, so two profiles on one machine would report different lists. Pixelscan read that as masking. We bisected it on one profile with controls at both ends, and only the font hiding flipped the verdict, so patch `0131` removed it. [Pixelscan's masking verdict, bisected](https://mosaicbrowser.com/blog/pixelscan-masking-bisected) has the run. The per-profile jitter on glyph widths stayed, because it changes the exact sizes a page measures without changing which fonts exist.

That changes what granting `local-fonts` means. In a profile it hands the site the real list of this computer, the same list every other profile here would return. If a site needs it, a font picker in a design tool for example, grant it in that profile and nowhere else. The grant is remembered per site and per profile, and `permissions.query` will report it on every later visit. What tells two profiles apart is the rest of the [browser fingerprint](https://mosaicbrowser.com/glossary/browser-fingerprint) and above all their separate cookie jars.

## Questions

### What permission does the Local Font Access API need?

The permission is called local-fonts. Calling window.queryLocalFonts() asks the user for it, and the call also needs a user gesture such as a click and a document that the local-fonts Permissions-Policy allows.

### Can a site check whether I granted local-fonts without asking?

Yes. navigator.permissions.query({name: 'local-fonts'}) returns granted, prompt or denied with no prompt at all. The answer is per site, so a site learns only what you decided for it.

### Does denying the permission stop font fingerprinting?

No. A script can still find out which fonts are installed by measuring text drawn in each candidate font against a fallback. That needs no permission and shows nothing on screen, so denying the prompt only stops the API itself.

### Which browsers have queryLocalFonts?

Desktop Chromium browsers: Chrome, Edge and others built on Chromium. Firefox and Safari do not ship it, and neither does Chrome on Android, so on those the function is simply absent.

## Related

[How websites list your fonts without a font API](https://mosaicbrowser.com/blog/how-sites-detect-installed-fonts): Websites detect installed fonts by measuring a test string in each font against a fallback. A width that changes means the font is there. No permission needed.

[Pixelscan's masking verdict, bisected to one surface](https://mosaicbrowser.com/blog/pixelscan-masking-bisected): We bisected Pixelscan's masking detected verdict on one profile. Canvas, WebGL, audio and client rects were fine. Hiding about 2% of font families caused it.

[344 profiles said "denied": one value, everywhere](https://mosaicbrowser.com/blog/notification-permission-denied): Notification permission became a fingerprint: Notification.permission read denied on all 344 of our profiles, where an unconfigured Chrome says default.

[Browser fingerprint: what a site can read about your machine](https://mosaicbrowser.com/glossary/browser-fingerprint): A browser fingerprint is the set of values a site reads about your browser and computer, which together recognize you without cookies. What goes into one.

[Why hCaptcha gives some browsers a puzzle every time](https://mosaicbrowser.com/blog/hcaptcha-image-challenge-every-time): hCaptcha's challenge is its score. A puzzle every time usually means blocked cookies, a crowded VPN exit or a browser whose values disagree with each other.

[Why every Mac's Chrome says Mac OS X 10_15_7](https://mosaicbrowser.com/blog/mac-os-x-10-15-7): Chrome froze the macOS version in its User-Agent, so every Mac claims 10.15.7. The real one is in Sec-CH-UA-Platform-Version, a hint sites must ask for.

[Why the User-Agent says Windows 10 on Windows 11](https://mosaicbrowser.com/blog/windows-11-user-agent-windows-10): Windows 11 and 10 both send Windows NT 10.0 in the User-Agent. Only the platformVersion client hint, 13 or higher, says Windows 11.

[Why Chrome's User-Agent says 0.0.0](https://mosaicbrowser.com/blog/chrome-user-agent-0-0-0): Chrome zeroes everything after the major, so the User-Agent says 0.0.0 and the build moves to Sec-CH-UA-Full-Version-List. A real build in the UA is a tell.

## Mosaic, on your own computer

The browser this blog is about: its own Chromium, fingerprints from real machines and a proxy exit per profile. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
