The shop keeps its invoices as files in the folder invoices/. The route GET /invoices/<path:name> joins the folder and name and sends that file back: /invoices/2026/order-1.txt reads invoices/2026/order-1.txt. If there's no such file, it answers 404.
The attack: in a path, .. means the folder one level up. A visitor asks for /invoices/../secrets.txt with a tool that sends the path as typed, or writes the slash as %2F, which Flask decodes. The path climbs out of invoices/, and the attacker gets the shop's secret key and supplier API key; ../invoices-all.csv gives them every customer's email.
The fix has two parts. os.path.realpath works out every .. and gives the real full path, the file the server would really open. Then that path must still start with the folder and a slash: without the slash, invoices-all.csv, next to the folder, would pass too. On another folder:
os.path.realpath("/shop/photos/2026/../../app.py") # "/shop/app.py"
The route must answer:
GET /invoices/2026/order-1.txt → 200 Invoice for order 1 …
GET /invoices/2026/order-2.txt → 200 Invoice for order 2 …
GET /invoices/2026/order-99.txt → 404 {"error": "Invoice not found"}
GET /invoices/../secrets.txt → 404 {"error": "Invoice not found"}
GET /invoices/2026/../../secrets.txt → 404 {"error": "Invoice not found"}
GET /invoices/..%2Fsecrets.txt → 404 {"error": "Invoice not found"}
GET /invoices/../invoices-all.csv → 404 {"error": "Invoice not found"}
Your task: in get_invoice, below the os.path.join line, turn path into its real full path. When it doesn't start with INVOICES + os.sep, answer {"error": "Invoice not found"} with status 404, just like a missing invoice.
Assignment
Follow the Coding Tutorial to work out the real path with os.path.realpath and refuse any path outside the invoices folder.
HintINVOICES is already a real path, so both sides are written the same way. os.sep is the slash, "/".
Free plan: 30 tutor messages. PRO is unlimited.
