What browser does not do¶
The gate is narrow on purpose, and a narrow guarantee is only useful if its edges
are written down. This page is the list of things people reasonably expect from a
module called browser that it does not do — and, where the omission is a
decision rather than an oversight, why.
The short version: it checks that a URL is boring before the operating system sees it. It does not judge where the URL points, what happens after it is opened, or whether opening it was a good idea.
Can I add a scheme to the allowlist?¶
No. Not through configuration, not through an option, not through an environment
variable. https, http, and mailto are the whole set, and widening it means
changing this module's source and passing a security review.
This is the most common request and the answer will not change, because a
configurable allowlist would defeat the point. The value of a single validation
gate is that reading one function tells you every scheme that can reach the
operating system in the entire program. The moment a config key can add
myapp://, that guarantee becomes "whatever the deployment's config file says",
and the audit has to follow the config instead of the code.
If you genuinely need a custom scheme — an internal protocol handler, a
desktop-app deep link — supply your own Opener
and route that scheme around this module entirely, with your own justification
for it. Do not try to smuggle it through the gate.
Does it check where the URL points?¶
No. Once the scheme is on the allowlist, the host is not examined at all. There is no host allowlist, no denylist, no DNS resolution, no check for private or loopback addresses, and no reputation or safe-browsing lookup.
http://localhost:8080, http://169.254.169.254/latest/meta-data/, and
https://an-attacker-controls-this.example all pass. So does an
internationalised domain that looks exactly like your bank's, and so does a URL
containing a right-to-left override that makes it display as something else.
The reasoning is that this is a boundary check on a string, not a policy engine about destinations. Deciding which hosts a user may visit depends entirely on the application — a documentation link, an OAuth redirect and a user-submitted link want three different policies — so it belongs in the application. What the gate promises is narrower and checkable: the operating system will never be asked to dispatch a scheme you did not intend.
Note that this is a different bar from the one an outbound HTTP client has to meet. Nothing here fetches the URL; a human's browser does, in a sandbox, under their control. Server-side request forgery is not the threat being defended against.
Does a URL have to have a host?¶
No, and this surprises people. https: and https:// are both accepted, because
net/url.Parse accepts them and their scheme is on the list. So is mailto:
with no address.
The gate's question is "could this string dispatch something dangerous", not
"will this URL work". A hostless https: URL is harmless — the browser opens and
does nothing useful. If your application needs a real destination, check for one
before calling OpenURL.
The same goes for userinfo: https://user:pass@example.com passes untouched. The
credentials are not stripped, redacted, or warned about.
Does it protect mailto: from header injection?¶
No, and this is the sharpest edge in the module. mailto: is on the allowlist,
but the gate never parses what comes after it — subject=, body=, cc=, and
bcc= are opaque to it. A bcc= smuggled in from user-influenced data reaches
the mail client exactly as written.
Escaping every parameter with url.QueryEscape is the caller's job, every time,
and there is a worked example in
Open URLs safely.
It stays the caller's job because doing it here would mean parsing and re-encoding the URL, and the gate deliberately hands the opener the exact bytes it was given. A module that silently rewrote your URL would be harder to reason about than one that refuses to.
Can I cancel an open once it has started?¶
No. The context is checked once, immediately before the opener runs. After that there is nothing to cancel: a browser process belongs to the operating system's session, not to your program, and no platform handler offers a "close that window again" call.
Opener has no context.Context parameter for the same reason — a signature
that accepted one would imply a cancellation it cannot deliver.
Does it tell me whether the page opened?¶
Only whether the launcher reported success. A nil return means the platform
handler accepted the URL and exited without error. It does not mean a browser
window appeared, the page loaded, the user noticed it, or the URL resolved.
There is no callback, no window handle, and no way to poll. If your workflow depends on the user actually arriving somewhere — an OAuth flow, say — you need the other end of that flow to tell you, not this module. Printing the URL alongside the open is a cheap safeguard: it costs a line and rescues every user whose default browser is broken.
Can I open a local file or an HTML string?¶
Not through this module. cli/browser, the dependency underneath, offers
OpenFile and OpenReader; neither is re-exported here, and OpenFile produces
a file:// URL that the allowlist would reject anyway.
That is the intended outcome rather than a gap. file:// is on the excluded list
precisely because an attacker-chosen local path is a disclosure risk, and a
convenience wrapper that produced one would be a hole in the same fence. If you
need to show a user a local HTML file, write it somewhere they can open
themselves and tell them where it is.
Can I turn opening off globally?¶
No. There is no global switch, no environment variable, and no package-level
opener to replace. WithOpener applies to the single call it is passed to.
A --no-browser flag is a normal thing for a CLI to want, and it belongs in the
CLI: check the flag and print the URL instead of calling OpenURL. Keeping the
decision at the call site is what stops one part of a program disabling opening
for another part that was relying on it — and it is why parallel tests using
WithOpener cannot interfere with each other.
Does it log anything?¶
Nothing. No logger, no telemetry, no metrics, no counter of opens. The module does not import a logging library and none of its errors contain the URL.
If you want an audit trail of URLs opened, wrap OpenURL in your own function
and log there — where you can decide how much of the URL is safe to record. Logging
the scheme and host, and not the path or query, is the usual answer, because paths
and queries carry tokens.
Is this a browser automation library?¶
No. It opens a URL and stops. It cannot drive a page, read a page, fill a form, take a screenshot, or wait for anything. Nothing in it inspects the browser after launch, and there is no headless mode.
If you need to control a browser, you want a WebDriver or CDP client. If you need
to fetch a URL, you want net/http. This module exists for the single moment
where a program hands a URL to the human's own browser.
What is not regression-guarded here¶
Two things this documentation describes are not held in place by a test in this repository:
- Per-platform opener behaviour. Every test injects a fake opener, so the
default path —
open,xdg-open,ShellExecute— is never exercised. Acli/browserupgrade could change what runs on Windows or which Linux providers are tried without any test here noticing. See Platform behaviour. - The caller discipline. Nothing in this module can stop a caller invoking
exec.Command("open", url)directly and bypassing the gate entirely. The guarantee is only as good as the codebase's habit of routing throughOpenURL, which is a review and lint concern, not something the code enforces.
See also¶
- Threat model & the validation gate — what the module does defend against, and why it is shaped this way.
- Validation rules — the exact accept/reject boundary.
- Open URLs safely — the caller-side work these limits leave you.