Commit 2f45c34
fix(consent): authenticate the consent flow and make
The Azure admin-consent callback is unauthenticated, and it has to be —
Microsoft redirects the browser to it, so there is no session to present.
Everything it then does is driven by its query parameters, and `state`
was the bare authority id: echoed, never verified.
That let anyone who could reach the deployment reconfigure any authority.
The attacker needs no PlaceOS credentials at all: sign up for a free
Entra tenant, grant admin consent to the PlaceOS app as its own global
admin (the product's intended flow, open to any Microsoft admin), read
the victim's authority id from the unauthenticated /auth/authority
endpoint, then issue one GET at the victim's host with `state` set to it.
The flow builds an oauth strat pointing at the attacker's directory and
writes it to `authority.login_url`, so every user of that domain is
redirected from the genuine PlaceOS URL into an identity provider the
attacker controls. There is no Host binding either, so one request from
anywhere can retarget any authority in the deployment, and a captured
callback URL replays unchanged.
The stacked #442 work makes this materially worse: `upsert_calendar_tenant`
blind-overwrites the staff-api tenant's `platform`, `delegated` and
`credentials`. The same anonymous request therefore replaces a live
customer's Microsoft Graph credential with one minted in the attacker's
directory — encrypted in place with no prior value retained, so it is
unrecoverable — and repoints PlaceOS's server-side calendar client at a
directory they own.
Two changes:
- Starting a flow now requires an administrator. `index` was in the
`skip_action :authorize!` list along with the callback; only the
callback needs to be there.
- `state` is now an opaque single-use token (`ConsentState`) rather than
the authority id. It exists only because an authenticated admin asked
to start a flow for a specific authority, names that authority server
side rather than in the URL, expires after 15 minutes, and redeeming it
destroys it. The callback redeems rather than trusting, so an unknown,
expired or replayed state is refused. Redemption uses the delete's
reply count so a concurrent replay cannot slip through the window
before the delete lands.
Specs cover redeem-once, refuse-twice and refuse-unissued. They could not
be run locally — the Docker spec harness OOMs compiling this repo — so
they run in CI. `crystal build --no-codegen` passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>state a single-use token1 parent 2d64a87 commit 2f45c34
3 files changed
Lines changed: 112 additions & 4 deletions
File tree
- spec
- src/placeos-rest-api
- controllers
- utilities
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
5 | 35 | | |
6 | 36 | | |
7 | 37 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
10 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
11 | 18 | | |
12 | 19 | | |
13 | 20 | | |
| |||
29 | 36 | | |
30 | 37 | | |
31 | 38 | | |
32 | | - | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
33 | 44 | | |
34 | 45 | | |
35 | 46 | | |
| |||
46 | 57 | | |
47 | 58 | | |
48 | 59 | | |
49 | | - | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
50 | 71 | | |
51 | 72 | | |
52 | 73 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
0 commit comments