I was playing a CTF which was about path traversal. The server code was like below:
import flask
import os
app = flask.Flask(__name__)
@app.route("/docs/<path:path>", methods=["GET"])
def challenge(path="index.html"):
print(path)
requested_path = app.root_path + "/files/" + path
try:
return open(requested_path).read()
except PermissionError:
flask.abort(403, requested_path)
except FileNotFoundError:
flask.abort(404, f"No {requested_path} from directory {os.getcwd()}")
except Exception as e:
flask.abort(500, requested_path + ":" + str(e))
app.run("localhost", 3000)
The first and most obvious solution came to my mind was to request a url like http://localhost:3000/docs/../../flag. But it failed and the below was the corresponding log:
127.0.0.1 - - [03/Oct/2025 23:59:52] "GET /flag HTTP/1.1" 404 -
According to the log, my request is normalized/sanitized (not sure the correct word) and from /docs/../../flag has turned into /flag.
My question is at which stage my url has been normalized? I'm not using any webserver in the middle. Does flask's dev server perform any kind of normalization on the urls?
Note that I'm not seeking to find the solution for the CTF, I've solved it, my question is only about where is my url sanitized in the above code.
http://localhost:3000/docs/../../flag? When I use a browser to send that request, it's the browser that performs the normalization before the GET request is sent.curldoes URI normalization.