skip to content

What does filepath.IsLocal report about a path name, and which names does it reject?

level: middleimportance: should knowfreq 35%

answer

  1. one predicate over one untrusted element
  2. lexical only, nothing is opened
  3. empty and absolute are both out
  4. Windows adds device names to the reject list
  5. true means Join keeps you under base

basics

~20 s

filepath.IsLocal reports, by lexical analysis only, whether a name stays inside the directory it is evaluated in. It is false for the empty string, absolute paths, anything escaping via "..", and on Windows reserved device names.

solid answer

~50 s

`filepath.IsLocal(name string) bool` answers one narrow question: could this name, treated as relative to some directory, refer to anything outside that directory? It returns false for the empty string, for an absolute path, for a name whose `..` elements escape (`../x`, `a/../../x`), and on Windows for reserved device names such as `NUL` and for volume-qualified names. It returns true for ordinary relative names, including ones with harmless interior `..` such as `a/../b`. The documented payoff is precise: if `IsLocal(name)` is true, `filepath.Join(base, name)` is contained within `base`. It is still purely lexical — it opens nothing and knows nothing about symlinks — so it is a filter on untrusted names, not the confinement boundary itself. For a slash-separated name from an archive entry or a URL, `filepath.Localize` performs the same check and converts the name to an OS path, returning an error when it is not local.

code

go · 12 lines
go
filepath.IsLocal("reports/2026/q1.csv") // true
filepath.IsLocal("a/../b")             // true: nets out inside
filepath.IsLocal("../secrets/key.pem")  // false: escapes
filepath.IsLocal("/etc/passwd")         // false: absolute
filepath.IsLocal("")                    // false: empty

// For a slash-separated name off the wire, check and convert at once.
name, err := filepath.Localize(entryName)
if err != nil {
	return fmt.Errorf("rejecting entry %q: %w", entryName, err)
}
_ = name

go deeper

for a junior

Recall the signature and the shape of the answer: it takes one name, returns a bool, and says false for empty, absolute and escaping names. Know that you call it on the untrusted piece, not on a path you already built.

for a middle

Explain the exact guarantee — true implies Join(base, name) stays under base and Clean(name) has no leading .. — and that it is lexical. Be able to walk a/../b versus a/../../b and to name the Windows-only rejections.

for a senior

Position it correctly in a real pipeline: a fast rejection that yields a good error message naming the offending entry, sitting in front of a handle-based open that provides the actual guarantee. Explain why the validated name and the opened name must be the same string.

for a principal

Decide where this check lives so it cannot be skipped — one shared entry point rather than a convention — and be ready to justify what you do with rejected input: hard failure, quarantine, or a per-tenant error budget.

## The question IsLocal answers `func filepath.IsLocal(path string) bool` reports whether `path`, using lexical analysis only, has all of these properties: it is within the subtree rooted at the directory in which it is evaluated; it is not absolute; it is not empty; and, on Windows, it is not a reserved name such as `NUL`. The useful part is the guarantee that follows. If `IsLocal(name)` is true then `filepath.Join(base, name)` produces a path contained within `base`, and `filepath.Clean(name)` produces an unrooted path with no `..` elements. That is the exact property you want from an untrusted name before it becomes part of a real path, and it is the property `Join` and `Clean` on their own do not give you. ## What it rejects, concretely - **The empty string.** `IsLocal("")` is false. An empty element is not a name, and letting it through means a caller who sent nothing addresses the base directory itself. - **Absolute paths.** `/etc/passwd` is false. Note that `Join` would nest such an element rather than honour it, but a name arriving absolute is a name you did not expect, and IsLocal says so. - **Escaping `..`.** `../x` and `a/../../x` are false. The test is on the cleaned form: a name whose `..` elements consume more than the name itself built up escapes. - **Windows reserved and volume-qualified names.** On Windows, device names like `NUL`, `CON` and `COM1` are false, as are names carrying a volume. This is one of the few places where the same Go code makes a different decision per GOOS, and it is deliberate — those names would not stay inside the directory on that platform. What it accepts: ordinary relative names, and interior `..` that nets out inside, such as `a/../b`, which cleans to `b`. ## The slash-separated cousin Names that arrive over a wire are slash-separated regardless of the operating system you run on: archive entry names, URL path segments, keys from a manifest. `filepath.Localize(path string) (string, error)` is built for those. It takes a slash-separated path, checks the same locality property, and returns the equivalent path in the OS's own syntax — or an error if the name is not local. Using it means you do a single conversion-and-check instead of remembering to call `IsLocal` and then `FromSlash` in the right order. ## What IsLocal is not It is a **lexical** predicate. It opens nothing, stats nothing, and follows nothing. A name that IsLocal accepts can still reach outside the directory when the filesystem is asked to resolve it, because a component along the way may be a symlink pointing elsewhere. IsLocal cannot know that, and it does not claim to. So the role it plays in real code is a *filter*, not a *boundary*. Reject early and loudly with `IsLocal` — you get a clean error message naming the offending entry, and you never construct a nonsense path at all — and then perform the actual open through a directory handle (`os.OpenRoot`, whose `Root.Open`, `Root.Create` and `Root.OpenFile` fail on anything that leaves the root, symlinks included). The two are complementary: the predicate gives you a good diagnostic, the handle gives you the guarantee. ## Where it shows up Anywhere a name crosses a trust boundary and then becomes part of a filesystem path: entries in a submitted archive, upload filenames, a template or plugin name from a request, a key from a job payload. In each case the right shape is the same — validate the *element* you were handed, never the string you built out of it, and let the failure be a rejection of that entry rather than a path that quietly points somewhere else. A final practical note: on the accept path, keep the name you validated and the name you use identical. Re-deriving a name after the check (re-joining, re-cleaning, appending an extension by string surgery) throws away the property the check established.

  • Does filepath.IsLocal return true for a name containing a ".." element, such as a/../b?
    Yes. The test is whether the name escapes, not whether `..` appears. `a/../b` cleans to `b`, which is inside, so IsLocal is true. `a/../../b` cleans to `../b`, which escapes, so it is false. Treating any occurrence of `..` as fatal is a stricter rule you may choose, but it is not what IsLocal does.
  • Why does filepath.IsLocal give different answers on Windows for some names?
    Because locality is platform-defined. On Windows a reserved device name such as `NUL` or `COM1`, and a volume-qualified name, do not denote a file inside the directory at all, so IsLocal reports false for them; a Unix build has no such names and accepts them. Cross-platform code should validate on every target, not just on Linux.
  • If IsLocal returns true, is the resulting open guaranteed to stay inside the directory?
    No. IsLocal is lexical and never touches the filesystem, so a component that turns out to be a symlink can still lead elsewhere when the kernel resolves the path. Use IsLocal to reject bad names with a clear error, and open through an `os.Root` handle so the enforcement happens at open time.

saying these in an interview costs you the question

  • Says IsLocal checks the filesystem or follows symlinks
  • Thinks any occurrence of .. makes IsLocal false
  • Believes IsLocal accepts the empty string
  • Assumes the answer is identical on every GOOS
  • Calls IsLocal on the joined path instead of the element
  • Treats IsLocal as sufficient confinement on its own