go 50 lines · 9 steps

Preventing path traversal in Go

A SafeStore confines user-supplied paths to a base directory, rejecting anything that tries to escape.

Explained by highlit
1package storage
2 
3import (
4 "errors"
5 "path/filepath"
6 "strings"
7)
8 
9var (
10 ErrEmptyPath = errors.New("storage: empty path")
11 ErrTraversal = errors.New("storage: path escapes base directory")
12 ErrAbsolute = errors.New("storage: absolute paths are not allowed")
13 ErrNullByte = errors.New("storage: path contains null byte")
14)
15 
16type SafeStore struct {
17 root string
18}
19 
20func NewSafeStore(root string) (*SafeStore, error) {
21 abs, err := filepath.Abs(root)
22 if err != nil {
23 return nil, err
24 }
25 return &SafeStore{root: filepath.Clean(abs)}, nil
26}
27 
28func (s *SafeStore) Resolve(userPath string) (string, error) {
29 if userPath == "" {
30 return "", ErrEmptyPath
31 }
32 if strings.ContainsRune(userPath, '\x00') {
33 return "", ErrNullByte
34 }
35 if filepath.IsAbs(userPath) {
36 return "", ErrAbsolute
37 }
38 
39 joined := filepath.Join(s.root, filepath.Clean("/"+userPath))
40 
41 rel, err := filepath.Rel(s.root, joined)
42 if err != nil {
43 return "", err
44 }
45 if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
46 return "", ErrTraversal
47 }
48 
49 return joined, nil
50}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Anchoring to an absolute, cleaned root gives you a stable base to measure every resolved path against.
  2. 2Layering cheap rejections (empty, null byte, absolute) before the real join keeps obviously bad input out early.
  3. 3Checking the relative path for a `..` prefix is the definitive test that a path stays inside its base.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Preventing path traversal in Go — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code