Module 7: Information flow (Tainting)#
You are in the CPEX tutorial. This module needs the IdP.
Goal: carry security state across a session so a later request can be denied because of what an earlier one did. This is how CPEX stops write-down.
The problem#
Most authorization systems judge each request alone. That misses a whole class of leaks: a caller reads sensitive data, then sends it out through a channel that looks harmless on its own. You want the second call blocked because of the first, even though nothing about the second call is suspicious in isolation.
Build it#
One route taints the session; another refuses when the taint is present. From policies/m07.yaml:
routes:
- tool: get_compensation
authentication: [keycloak]
authorization:
pre_invocation:
- "require(role.hr)"
- "taint(secret, session)" # mark the session as having read secret data
- tool: send_email
authentication: [keycloak]
authorization:
pre_invocation:
- "require(authenticated)"
- "security.labels contains \"secret\": deny('write-down blocked: this session read secret data', 'session_tainted')"taint(secret, session) records the secret label on the session, and it persists in the session store. send_email reads security.labels, the accumulated session state, not just this request’s arguments. The label outlives the request that set it.
Run it#
cargo run -p cpex-tutorial --example m07_tainting▸ alice, fresh session → send_email (nothing tainted yet)
✓ ALLOWED {"sent":true, ...}
▸ alice, session-hr-work → get_compensation (taints session 'secret')
✓ ALLOWED { ... }
▸ alice, session-hr-work → send_email (write-down blocked)
✗ DENIED [session_tainted] write-down blocked: this session read secret dataThe same send_email call is allowed in a clean session and denied in the session that read compensation. The email’s content is irrelevant. The session’s history blocks it.
Try it#
- Separate the sessions. In
examples/tutorial/examples/m07_tainting.rs, change the third call’s.in_session("session-hr-work")to a fresh id like.in_session("session-new")and re-run. Expect: the email allows again, because taint is per session. - Rename the label. In
examples/tutorial/policies/m07.yaml, changetaint(secret, session)totaint(pii, session)and update the guardsecurity.labels contains "secret"to"pii". Re-run. Expect: the write-down still blocks, now keyed on thepiilabel. Labels are arbitrary names you choose; the write and the read just have to agree. - Persist across restarts. The default store is in-memory and resets when the program exits. To back it with Valkey:
- Start Valkey:
docker compose -f examples/tutorial/idp/docker-compose.yml --profile valkey up -d. - Enable the feature: in
examples/tutorial/Cargo.toml, change thecpexdependency tofeatures = ["builtins", "valkey"]. - In
policies/m07.yaml, add a session store underglobal:global: apl: session_store: kind: valkey endpoint: localhost:6379
- Start Valkey:
Checkpoint#
Why is the email denied when its own content is harmless?
What ties the two calls together?
Go deeper#
Next#
Module 8: Human in the loop: suspend an operation until a human approves it.