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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Anchoring to an absolute, cleaned root gives you a stable base to measure every resolved path against.
- 2Layering cheap rejections (empty, null byte, absolute) before the real join keeps obviously bad input out early.
- 3Checking the relative path for a `..` prefix is the definitive test that a path stays inside its base.
Related explainers
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
go
package logging import ( "context"
Deduplicating log attributes in Go's slog
decorator-pattern
structured-logging
immutability
Intermediate
8 steps
php
<?php final class RotatingFileLogger {
How a rotating file logger works in PHP
logging
file-rotation
io
Intermediate
9 steps
go
package middleware import ( "net/http"
Per-plan export limits in Gin middleware
middleware
rate-limiting
authorization
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/preventing-path-traversal-in-go-explained-go-e386/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.