Invoice 1041 and invoice 1042
You pay a bill online and the address of the page ends in 1041. Out of curiosity you change it to 1042.
The page shows somebody else's bill: their name, their address, what they bought. You did not break anything. The site simply never asked whose bill it was.
The one rule
Warning
This lab is a shop that is insecure on purpose. Attack your own private copy and nothing else. It has no way out to the internet. Never try these things on a site that is not yours: in the UK that is a crime under the Computer Misuse Act 1990.
By the end of this lesson
Note
I can say whether a request should have been allowed.
Key words
| Word | What it means | Example |
|---|---|---|
| Authentication | proving who you are | a password |
| Authorisation | checking what you are allowed to do | |
| Role | a named set of rights | admin |
| Identifier | a number or name that picks out one record | order 17 |
| Token | a signed value a browser sends to show it has signed in | |
| Status code | a number at the start of a reply that says how it went | 401 |
The cells students fill in are left blank in this preview.
Signing in is not being allowed
A site asks two questions. The first is authentication: who are you? The second is authorisation: may you do this?
Many weak sites ask the first and forget the second. They know you are a customer, so they show you whatever you ask for, including what belongs to other customers.
A status code says how a request went
Every reply starts with a status code. 200 means here it is. 401 means the site does not know who you are. 403 means it knows, and the answer is no.
The Runs below ask your lab's shop for three things without signing in. Watch which ones it hands over.
A page for everybody: one product
curl -s -o /dev/null -w "%{http_code}\n" http://juice-shop:3000/api/Products/1Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
A page for signed-in people: the user list
curl -s -o /dev/null -w "%{http_code}\n" http://juice-shop:3000/api/UsersShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
What the shop says when it refuses
curl -s http://juice-shop:3000/rest/basket/1 | grep -o "<title>[^<]*</title>"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Worked example: reading the three answers
| Request | Status | What it tells you |
|---|---|---|
| /api/Products/1 | 200 | A product is public, so anybody may read it |
| /api/Users | 401 | The shop wants to know who is asking before it lists people |
| /rest/basket/1 | 401 | A basket is private. The shop wants a token first |
The cells students fill in are left blank in this preview.
An id in an address can be changed
Orders, baskets and invoices have numbers. The number often sits in the address. Anyone can change it.
So the site must check, on every request, that this account owns this record or holds a role that may read it. A number that is hard to guess is not a check.
Worked example: a short request log
| User | Role | Request | Allowed? |
|---|---|---|---|
| ana | customer | GET /orders/17 (hers) | Yes, she owns it |
| ana | customer | GET /orders/18 (Bob's) | No, it is not hers |
| sam | admin | GET /orders/18 | Yes, an admin may read any order |
| bob | customer | GET /admin/users | No, only an admin may |
The cells students fill in are left blank in this preview.
A common mistake
Warning
Many people think hiding a button or a link protects a page. It does not. A browser can ask for any address, whether a link points to it or not. The check must be in the server code that answers the request. Hide the link too, but never rely on it.
The server checks every request
A safe handler does three things in order. It finds out who is asking. It loads the record. It checks the owner or the role, and refuses if neither fits.
Refuse with 403. Do the check on the server, in one place, so that no page forgets it.
An owner-or-admin check
def may_read(user, role, owner):
if role == "admin":
return True
return user == owner
# Change the names and press Run. Then let a "support" role read too.
print(may_read("ana", "customer", "ana"))
print(may_read("ana", "customer", "bob"))
print(may_read("sam", "admin", "bob"))