Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
392: Safe Handling of User Input
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}")
There are no comments for now.