Skip to Content
Course content

392: Safe Handling of User Input

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was building a small internal tool the other day—just a simple log viewer that lets a developer specify which log file they want to see from a specific directory. My first instinct was to keep it lean. I figured, "I'll just take the filename from the user and open it."

Trusting the User Too Much

Here is exactly how I started. I had a folder called /logs/ and I just wanted to append the user's input to that path.

import os

log_dir = "logs/"
filename = input("Which log file do you want to read? ")
full_path = log_dir + filename

with open(full_path, 'r') as f:
    print(f.read())

I tested it with app.log. It worked perfectly. I tested it with error.log. Still perfect. At this point, I felt like I was done. But then I remembered a rule I learned years ago: Never trust user input. I decided to try and "break" my own code.

The Escape Trick

If I'm a malicious user (or just a curious one), I don't have to give you a filename. I can give you a path. I tried entering this as the filename:

../secrets.txt

Suddenly, the program wasn't looking in the logs/ folder anymore. Because of the ../ (which tells the OS to go up one directory), the path became logs/../secrets.txt, which effectively resolves to just secrets.txt in the root folder. I just read a file that was never meant to be public. This is called a Path Traversal attack, and it's a classic way systems get compromised.

The Filter Fallacy

My immediate reaction was to just block the "bad" characters. I thought, "Easy, I'll just make sure the user didn't type ..".

filename = input("Which log file do you want to read? ")

if ".." in filename:
    print("Nice try! No directory traversal allowed.")
else:
    full_path = log_dir + filename
    # ... open file ...

That feels safer, right? Wrong. I tried entering an absolute path instead: /etc/passwd (on Linux) or C:/Windows/win.ini (on Windows). Since I was just concatenating strings, the resulting path was logs//etc/passwd. Most operating systems are surprisingly forgiving; they see that leading slash and ignore the logs/ prefix entirely. I was right back where I started.

Locking Down the Sandbox

I realized I shouldn't be trying to guess what "bad" input looks like. Instead, I need to force the input to be exactly what I expect: a simple filename, nothing more.

The os.path.basename() function is a lifesaver here. It strips away everything except the actual name of the file, regardless of what path the user tried to sneak in. I also switched to os.path.join() because it handles slashes more reliably across different operating systems.

import os

log_dir = os.path.abspath("logs")
user_input = input("Which log file do you want to read? ")

# This strips out any path info, leaving only the filename
safe_filename = os.path.basename(user_input)
full_path = os.path.join(log_dir, safe_filename)

# One last check: make sure the resulting path is actually inside our log_dir
if os.path.commonpath([log_dir]) == os.path.commonpath([log_dir, full_path]):
    try:
        with open(full_path, 'r') as f:
            print(f.read())
    except FileNotFoundError:
        print("File not found.")
else:
    print("Access denied: You cannot leave the log directory.")

Now, if I enter ../../etc/passwd, os.path.basename turns it into just passwd. The code then looks for logs/passwd, doesn't find it, and throws a FileNotFoundError instead of leaking system secrets. That's the difference between "filtering" and "sanitizing."




📋 Practical Task

Exercise: Securing the User-Defined Log Path

You have been handed a legacy script that allows users to download a configuration file from a /configs/ directory. However, the current implementation is vulnerable to path traversal.

Your Goal: Modify the get_config_content function to ensure that the user cannot access any files outside of the configs directory, even if they use ../ or absolute paths.

import os

def get_config_content(user_filename):
    # VULNERABLE CODE:
    # config_dir = "configs"
    # path = config_dir + "/" + user_filename
    # with open(path, 'r') as f:
    #     return f.read()
    
    # YOUR CODE HERE:
    pass

# Test cases to verify your fix:
test_inputs = ["settings.conf", "../../etc/passwd", "/etc/shadow", "subfolder/hidden.txt"]
for inp in test_inputs:
    try:
        print(f"Input: {inp} -> Content: {get_config_content(inp)}")
    except Exception as e:
        print(f"Input: {inp} -> Blocked/Error: {e}")
Rating
0 0

There are no comments for now.

to be the first to leave a comment.