0%

Who May Do What, and How Often · capstone

Capstone project: Project: Your List and Nobody Else's

Ownership, a login limit, and a log each work, and each has a test. This project states them as one policy, and closes the last gap in it: what happens when your own code fails.

The policy

RouteWho may call itA refusal looks likeLogged as
GET, HEAD /aboutanyone405 for another -
GET, HEAD /books, /books/{id}a token that verifies; the caller’s own books only401 without a token; 404 for a book that is missing or someone else’sthe token’s user
POST /books, PUT and DELETE /books/{id}a token whose user may write; the caller’s own books only401; 403 for an account that may not write; 404 for a book that is not the caller’sthe token’s user
POST /loginBasic credentials401; 429 with Retry-After when the name or the whole service is over its limitthe user after a correct password, - otherwise
POST /tokensa with a user401the session’s user
GET /me, POST /prefsanyonenonethe session’s user when the request reads a live session, - otherwise
POST /logoutanyonenone-

Every request that reaches handle() is written down once, in five fields and without a credential. The book reads answer private, no-cache, and the five session routes answer no-store.

When your own code fails

The table says what the server does with requests it understands. One case is missing: a mistake in the server itself. Today an error inside route() escapes handle(). The client gets no answer from your code, and the log gets no line.

handle() is the one place every response your routes build passes through, so the guard goes there. (The 411, 413, and 400 that read_body answers before a route runs do not pass through it, and are not logged.) Wrap the call to route in try. In except Exception:, build the answer:

  • traceback.print_exc() prints the to standard error, where the reads. The traceback is new, the project already imports it, and this one call is all you need from it.

  • json_error(500, ...) with a fixed message of your choice.

  • cache-control set to no-store.

  • An empty body when the method is HEAD. That is the rule from with Python, which route did not get to apply this time.

Keep the log line after the try, so that the failed request is logged too, as a 500. Lesson 5 wrote log_line so that it cannot fail, so nothing after the try can raise.

Why Exception, and not a bare except? Pressing Ctrl+C raises KeyboardInterrupt, which is how you stop a server from the keyboard. KeyboardInterrupt is not an Exception, so this guard lets it through, and the server still stops.

Python in Practice taught you to catch narrowly, and that still holds everywhere else. This is the one place where a wide catch is the right boundary, and it is the outermost one: whatever went wrong inside route(), the client gets an answer and the operator gets the detail.

The operator’s stream is the right place for a traceback. It is not a safe place. A traceback can hold a from a request, so the people who may read standard error should be few.

Why does the guard catch Exception and not every possible exception?

Milestones

1. The attack test. A client can already make your server raise. Send GET /books/ followed by 5,000 digits: int() refuses a number that long with a , the error escapes handle(), and the client gets no answer at all. A guard is for that mistake and for the ones nobody has found yet. The test uses a failure that is quicker to write down: it breaks the state by hand. With steps.keyed_state() and a token for ana, it removes the books with state.pop("books") and sends GET /books. It asserts status 500, and that neither b"Traceback" nor b"KeyError" is in the body. Run python -m pytest -q: 1 failed, 20 passed. The KeyError escapes handle() and fails the test.

2. The guard. Write it in handle, as described above.

3. Everything together. python -m pytest -q ends with 21 passed. Then run both scenarios:

python pl_sec.py other-reader
python pl_sec.py guessing

Each ends with your server’s log. In the first you can read ben’s attempt:

1000000 DELETE /books/1 404 ben

4. The findings. In SECURITY.md, rows F8 and F9 each name their test and say closed.

ben's refused delete is in the log as ... DELETE /books/1 404 ben. What does that line give an operator that the 404 alone did not?

What is still open

F14 from Chapter 5 is still open: a token that leaks works until it expires.

And look at the books once more. A title may be any text a client sends, up to the body limit. Your server stores it and hands it back without a second look. The next chapter gives a book three more fields, and asks what happens when text from a request is read as something more than text.

Task

Close the last gap: an error in your own code must become a fixed 500.

  1. In tests/test_attacks.py, write test_attack_error_body_shows_no_traceback(). With state = steps.keyed_state() and bearer = steps.bearer_for(state, "ana", "correct horse"), remove the books with state.pop("books"), send GET /books with the token in the Authorization header, and assert that the status is 500 and that neither b"Traceback" nor b"KeyError" is in the body. Run python -m pytest -q: this test must fail first.

  2. In handle in reading_server.py, put the call to route inside try. In except Exception:, call traceback.print_exc(), build json_error(500, ...) with a fixed message, set its cache-control to no-store, and empty its body when the is HEAD. Keep the log line after the try, so that it is written for a 500 too.

  3. Run python -m pytest -q until it ends with 21 passed, then run python pl_sec.py other-reader and python pl_sec.py guessing.

  4. In SECURITY.md, make sure rows F8 and F9 each name a test and say closed.

Then press Check my work. It puts a that raises in place of reading_server.route and calls reading_server.handle(...): the answer must be status 500 with a fixed body that shows nothing of the error, no-store, an empty body for HEAD, a log line with status 500, and a on standard error; a KeyboardInterrupt must pass through. It repeats the checks of Lessons 2 to 5 with new users, books, and times. It runs the four tests of this chapter, selected by name, against servers it puts in place of reading_server.handle and reading_server.sample_state: all four must pass against a correct one, each must fail against a server with its own mistake, and all must fail against one that refuses every request. It reads rows F8 and F9 of SECURITY.md. Nothing from your is graded.