How My HTTP Server Almost Compromised My Machine
A deep dive into path traversal vulnerabilities.
Published on: 13 Jun, 2025
Motivation
Last year, I took a Systems Programming class at university and enjoyed it thoroughly. It was the first time I wrote any substantial code in C and I was impressed with how much control I had over my machine - from memory management to system calls - and I wanted more. Something the course touched on were client-server programs and the HTTP protocol. So, over the summer, I decided to build my own HTTP server from scratch. I’ve outlined my journey here. But this post isn’t about building my server - it’s about what happened after.
The Problem
void respond_to_client(int clientfd, char* path) {
if (strstr(request, "GET")) {
// some code
if ((ptr = strstr(path, "/files/"))) {
char file_buf[BUF_SIZE];
long size;
char* filepath = path + 1;
FILE *f = fopen(filepath, "r");
// stream the file to the client
}
// more code
}
}
Hidden in these lines is a bug so serious that it could compromise your whole computer. Can spot the vulnerability? No? Don’t beat yourself up, it’s not an OWASP Top 10 security risk for no reason. This is the problem:
char* filepath = path + 1;
FILE *f = fopen(filepath, "r");
Why? Let’s take a closer look.
The Setup
In my server, I’ve exposed the files directory to clients and they can GET/POST files from/into this directory.
This is what a typical HTTP request looks like:
GET /files/index.html HTTP/1.1
Let’s break this down:
GET - Request method - GET implies the client wants something from the server
/files/index.html - Request Path - this is the file the client wants
HTTP/1.1 - HTTP version
The most important value here is the request path.
My intention was to store the requested path into a variable filepath skipping the first /. Why? Because in C, when a path does not begin with /, it is treated as a relative path instead of an absolute path.
.
├── files
│ └── index.html
├── main.c
├── Makefile
└── README.md
Since the files directory is at the same level as my source code (main.c), I would not have to make any changes to the path and could query the file directly.
This works great!
The Attack Vector
So what was my oversight? Assuming all clients will be good actors and query files from within the files directory.
Let’s say a client sends this request:
GET /files/../main.c HTTP/1.1
My server would return its entire source code! Why? Let’s take a look at the directory structure again:
.
├── files
│ └── index.html
├── main.c
├── Makefile
└── README.md
In unix/linux, .. refers to the parent directory - so /files/.. would go one level up from within the files directory and as you can see that is the same level as main.c! The client has broken out of the files directory! The problem does not end there, this has indirectly given any client the ability to query any arbitrary file from the machine!
All unix/linux systems store some standard files like etc/passwd which stores user information for all the users using the machine. Although it might take a few tries to get the number of ..s right, any client would be able to access this file due to this vulnerability.
Example:
GET /files/../../../etc/passwd HTTP/1.1
So let’s try it!

Wait…that doesn’t work? Does that mean we didn’t have to worry about this after all? Not quite. Here’s why - we are saved by curl here. curl by default resolves paths before sending queries - so the query sent by curl is:
GET /main.c HTTP/1.1
instead of
GET /files/../main.c HTTP/1.1
This does not pass this check:
if ((ptr = strstr(path, "/files/")))
and hence returns a 404.
We can override this default behaviour of curl by adding the flag --path-as-is.

Now we can see it returns the whole source code!
The Solution
The fix to this problem is pretty simple. Resolve the path and check if it is within the files directory after resolution before sending the file to the client. realpath is a library function that does exactly this.
Here is how I fixed this vulnerability:
/*
Resolves filepath and checks if it is within accessible boundary
Input: filepath
Return: 1 - safe path
0 - unsafe
*/
int is_safe_path(char* path) {
char resolved_path[BUF_SIZE];
char accessible_directory[BUF_SIZE];
// resolve absolute path of accessible directory
if (realpath("./files", accessible_directory) == NULL) {
return 0;
}
if (realpath(path, resolved_path) == NULL) {
return 0;
}
// check if resolved path starts with accessible directory
return strncmp(resolved_path, accessible_directory, strlen(accessible_directory)) == 0;
}
With that in place:

we can see that this issue has now been fixed.
Conclusion
Experiences like these can definitely take a shot at your confidence and make you reflect on your skill as a software developer - that definitely happened with me! But, at the end of the day, I’m happy I learned about this while building a toy server rather than while working on a production codebase (whew!). I would also like to take this opportunity to emphasize on the importance of testing and code reviews as it was through the latter that I came to know about this vulnerability, and rest assured - this is one mistake I (and now hopefully you too) will not be making again!