0%

What Can Go Wrong · practice

An Attack Is a Test That Starts Red

The third attacker from Lesson 2 has a copy of the data and never sends a request. This lesson shows what that copy is worth. In the , run the course helper’s first scenario:

python pl_sec.py stolen-backup

pl_sec.py starts your own server on 127.0.0.1, sends it a few requests, and prints what happened. This is the output, except for the , which is new on every run:

pl_sec: stolen-backup, your server on 127.0.0.1 (a port the system chose)
--- two ordinary requests ---
ana logs in with her password       POST /login   -> 200
ana asks who she is, with a cookie  GET /me       -> 200  {"user": "ana"}
--- a copy of your server's state, as a thief would read it ---
books     2 books (not shown)
next_id   3
users     'ana': 'correct horse'
users     'ben': 'battery staple'
tokens    'tok-ana-writes-7Qx2': {'user': 'ana', 'can_write': True}
tokens    'tok-ben-reads-K9pz': {'user': 'ben', 'can_write': False}
sessions  'sJFKdwVWzmnyheWotZw-vA': {'user': 'ana'}
--- what the thief tries next: each stored value as that user's password ---
log in as ana with the value stored for ana  -> 200  the thief is logged in
log in as ben with the value stored for ben  -> 200  the thief is logged in
--- the code in reading_server.py that checks a password ---
            if credentials is None or state["users"].get(credentials["user"]) != credentials["password"]:
--- result ---
2 of 2 stored values worked as a password
serve() returned after 5 connections; the server is closed
pl_sec: every thread has stopped

Read the transcript

The first block is ordinary use: ana logs in and asks who she is. It is there so that the state has a live session in it.

The second block is the copy. It holds both passwords, both tokens, and ana’s session id, all readable. In the third block the thief does the first thing anyone would try: they send each stored as that user’s password, and both logins answer 200. The fourth block quotes the line of your server that checks a password.

The thief in the transcript logs in as ben. What did they need first?

Two findings

F1 comes from the two users lines. Exposed: every user’s password. Attack: anyone with a copy of the state reads the passwords and logs in as each user. Fix: store something that proves a password is right without being the password. Chapter 3 builds that.

F2 comes from the quoted line. The login compares the guess with the stored value using !=. Chapter 2, Lesson 3 explains why that comparison is a finding of its own. For now, record its four parts so the row exists. Exposed: how much of a guessed secret is right. Attack: != can answer sooner for a worse guess. Fix: compare a secret with hmac.compare_digest. Test: none yet.

From a finding to a test

The fourth part of a finding is its proof. In this course a proof is an attack test: a test that performs the attack and asserts that the attack is refused. While the weakness is there, the test fails. Once the weakness is closed, the test passes and keeps watch.

For F1 the test has three steps, the arrange, act, assert shape you know, with the act being an attack:

  1. Arrange. Build a state with reading_server.sample_state(). Then log in as ana with her password and assert that the status is 200. This first assertion is a precondition. It makes the test about a server on which ana’s password works.

  2. Attack. Take the copy: str(state) turns the whole state into text, as a backup would.

  3. Assert the refusal. correct horse must not be in that text.

Take the copy after the login. A server could also leak a password while handling the request, into a session for example, and a copy taken earlier would miss that.

tests/helpers.py saves you from building requests by hand. Read make_request and basic now. The rest of the file is for later chapters.

headers = {"Authorization": helpers.basic("ana", "correct horse")}
request = helpers.make_request("POST", "/login", headers=headers)

make_request builds the request that handle() receives, the same your tests had. basic(user, password) builds the Authorization value for Basic credentials.

Why does the test first log in and expect status 200?

Red is the right color today

When the test is written, run the file:

python -m pytest -q

It ends with 1 failed, and pytest shows why: 'correct horse' is contained here, followed by the piece of the state that holds it. That failure is the finding, made repeatable. Anyone can now run one command and see F1.

The test stays red until Chapter 3, Lesson 6, where you change how passwords are stored. The comment at the top of tests/test_attacks.py says so, and no check before then needs this test to pass.

You run your new attack test against the server as it is, and it passes. What does that tell you?

One limit, said plainly: this test proves that no stored value is the password as text. A server that stored the password in would pass it and protect nothing. Chapter 2 opens on exactly that confusion.

Why go through the module

The test file starts with import reading_server, and your test calls reading_server.sample_state() and reading_server.handle(...). Keep it that way, and never write from reading_server import handle.

The reason is how your test gets checked. Check my work runs your test against other servers: a weak one that stores plain passwords, a fixed one that keeps no readable password, and one that refuses every login, where the precondition must make your test fail. It puts them in place by replacing reading_server.handle and reading_server.sample_state. A test that fails on the weak server and passes on the fixed one has shown that it sees the weakness. A name imported on its own would still point at your server, and the swap would go unnoticed. A state dict you build by hand would not fit the other servers either, which is why the test starts from sample_state().

Your task

  1. In tests/test_attacks.py, write test_attack_backup_reveals_no_password() below the comment for this lesson, with the three steps above.

  2. Run python -m pytest -q and see it fail on the last assertion.

  3. Open SECURITY.md and fill rows F1 to F4. Take F1 and F2 from this lesson and F3 and F4 from Lesson 3, in your own words if you like. Keep each row on one line. Put the name of your test in F1’s Test cell, write none yet in the other three, and leave every Status at open.

Then press Check my work. You leave this chapter with four findings on file and one of them proven by a failing test. Chapter 2 builds the two tools the fixes need.

Task

Write the first attack test and record four findings.

In tests/test_attacks.py, write test_attack_backup_reveals_no_password():

  • Build a state with reading_server.sample_state().

  • Send POST /login for ana with her password correct horse through reading_server.handle(...), and assert that the status is 200. helpers.basic("ana", "correct horse") builds the Authorization , and helpers.make_request("POST", "/login", headers=...) builds the request.

  • Then turn the whole state into text with str(state) and assert that correct horse is not in it.

Run python -m pytest -q in the : the test must fail. It stays red until Chapter 3, Lesson 6.

Then fill rows F1 to F4 of SECURITY.md: F1 and F2 from what python pl_sec.py stolen-backup printed, F3 and F4 from Lesson 3. Keep one line per row, put the name of your test in F1’s Test cell, and leave every Status at open.

Then press Check my work. It runs your test four times, each time against a different server that it puts in place of reading_server.handle and reading_server.sample_state: one whose state holds plain passwords, where your test must fail; one that accepts ana’s password and keeps no readable copy of it, where your test must pass; one that copies the password into the at login, where your test must fail again; and one that refuses every login, where your test must fail too. It also reads SECURITY.md: rows F1 to F4 need text in their Exposed, Attack, and Fix cells, and F1 needs the name of your test. The wording of your findings is not judged, and nothing from your terminal is graded.