Input That Becomes Code · practice
Redirect Only to Your Own Site
Lesson 4 made sure that next cannot add a line to your reply. It did not ask where next points. Run the scenario again:
python pl_sec.py injection
Its second part is about that question:
--- a redirect: ana logs in, and next names another site ---
POST /login?next=%2F%2Fother.example%2Fwelcome -> 303
location: //other.example/welcome
a browser that follows this leaves your site for other.example
The target held %2F%2Fother.example%2Fwelcome. parse_qs decoded each %2F to a slash, so the //other.example/welcome. That is one clean line, and header_value lets it through.
location. It resolves the value against the // replaces the host. Only the scheme is kept, so the client goes on to https://other.example/welcome.
Now look at it from the reader’s side. Someone sends them a link to your login, with your host name in it. They check the name and type their password. The login is real and it succeeds, and the page that comes next belongs to someone else. That page can look like yours and ask for the password once more. A server that redirects to wherever a request says is an open redirect.
What a local path looks like
next exists to send the reader to another page of the reading /, like /books or /books?limit=5.
Four kinds of value are not local:
//other.examplenames a host, as you just saw.https://other.example/is a whole URL, with a scheme and a host./\other.examplestarts with one slash, but a browser treats a backslash in a URL as a slash. It reads//other.example.A value with a tab or a line break in it, such as
/, a tab, and then/other.example. A browser removes tabs and line breaks from a URL before it reads it, and what is left is//other.exampleagain.
The last two matter because of who follows the redirect. The value has to be safe for the program that reads it next, and that program is a browser.
Why is /\other.example refused although it starts with one slash?
Ask the parser, and do not trust it alone
You know urlsplit from the router. Its result has a scheme and a netloc, the host part, and both are empty for a plain path. Ask it about four values. In Python source, "/\\other.example" is the text /\other.example.
from urllib.parse import urlsplit
for value in ["/books?limit=5", "//other.example/x", "https://other.example/", "/\\other.example"]:
parts = urlsplit(value)
print(value, "| scheme: [" + parts.scheme + "] host: [" + parts.netloc + "]")
It prints:
/books?limit=5 | scheme: [] host: []
//other.example/x | scheme: [] host: [other.example]
https://other.example/ | scheme: [https] host: [other.example]
/\other.example | scheme: [] host: []
The first three are what you would expect. The fourth is the trouble: Python sees no host in /\other.example, and a browser does. Two parsers read the same text and disagree, and the browser is the one that acts on it.
So the urlsplit. It checks the shape of the text itself first, and asks urlsplit last, as a second opinion. Last matters for one more reason: urlsplit raises //, such as //[, and the slash rule has already answered those.
What does this print?
from urllib.parse import urlsplit
print("[" + urlsplit("//other.example/x").netloc + "]", "[" + urlsplit("/\\other.example").netloc + "]")
Allow one shape
Lesson 1 listed the keys a book may have and refused the rest. This is the same idea for shapes. Do not collect dangerous values. Describe the one shape that is safe, in four rules:
Printable ASCII only:
value.isascii() and value.isprintable().isprintable()isFalsefor a tab, a line break, and every other control character. The ASCII half also stops a letter such asé, becausehttp_adapterwrites headers as Latin-1 and such a character would not arrive as it was sent.No backslash anywhere.
One slash at the start, and not two.
No scheme and no host, according to
urlsplit.
safe_redirect_target(value) returns the value when all four hold. For anything else it returns "/". It never raises, and the login still succeeds: the password was right, so a strange next is not a reason to refuse it. The reader lands on the front page.
A next with a line break is not printable, so from now on it becomes / as well, and the login answers 303. The 400 of Lesson 4 can no longer be reached through login. If your test from Lesson 4 asserts that status, it fails now. Assert what the headers hold, as Lesson 4 asked. header_value stays in redirect(), for every other caller and for the code you write next month.
A correct password arrives with next=https://other.example. What should the server do?
Your task
In reading_server.py:
Write
safe_redirect_target(value)with the four rules, in that order. Test the start with: value[:1]is the first character, or""for an empty value, andvalue[:2]is the first two.In
login_response, pass the value through it:redirect(303, safe_redirect_target(params["next"][0])). Keep thetryaround it.
Run python -m pytest -q. It still ends with 23 passed: the attack test for this guard is part of the project in Lesson 8. Then run the scenario again:
--- a redirect: ana logs in, and next names another site ---
POST /login?next=%2F%2Fother.example%2Fwelcome -> 303
location: /
the redirect stays on this site
The first part now shows a 303 with location: / as well, and the result line says 2 of 4 attacks worked.
Then press Check my work. The third part of the scenario fetches a file that is not a cover. That is the next lesson.
Task
Make the login redirect only to a path on this site.
In reading_server.py:
safe_redirect_target(value)returnsvalueunchanged when all four rules hold, and"/"for every other. It never raises and never repairs a value. The value is printable ASCII:
value.isascii() and value.isprintable().It holds no backslash.
It starts with one slash and not with two:
value[:1] == "/"andvalue[:2] != "//".urlsplit(value)finds no scheme and no host:schemeandnetlocare both"".
login_response(request, user)writesredirect(303, safe_redirect_target(params["next"][0])), still inside itstry.
Run python -m pytest -q and python pl_sec.py injection in the
Then press Check my work. It calls safe_redirect_target() with values you have not seen: paths on this site, with a query, a fragment, a colon, or an encoded space, which must come back unchanged, and values that name another host, carry a scheme, hold a backslash, a tab, a line break, or a letter outside ASCII, or do not start with exactly one slash, which must all come back as "/". It calls handle() with its own users: a correct login whose next names another site, or holds an encoded line break, must answer 303 with location: / and start one next, a login without next, and a wrong password must answer as before. Nothing from your terminal is graded.