Eh2406 opened a new issue, #430: URL: https://github.com/apache/casbin-rs/issues/430
Hi, I am Co-Lead of the Rust Cargo Team and was recently hired by the Rust Foundation to help triage AI discovered bugs. This bug was found with AI, namely scrutineer and Fable. Except where specifically labeled this report was written by me. I am posting this as a public issue as I have not received a response to our attempts to reach out on https://security.apache.org/report-code/, by email, and on your discord. From the AI: > ## Summary > > Revoked access remains allowed in CachedEnforcer. The cached decision belongs to the `CachedEnforcer` wrapper, but several public lifecycle methods update only the wrapped `Enforcer` and do not emit `Event::ClearCache`. A previously cached allow therefore survives policy reloads, policy clearing, model or adapter replacement, role-manager changes, role-link rebuilds, and enforcement disable/enable transitions. > > ## Impact > > An authenticated user whose permission has been revoked can continue receiving authorization for a previously cached request tuple. The stale allow has no time-to-live and can persist until other distinct tuples evict it, the process restarts, or an unrelated management API mutation clears the cache; protected reads or writes represented by that decision may therefore remain available after the operator believes revocation has completed. > > ## Proof of concept > > Create a small Cargo binary depending on `casbin` with features `cached` and `tokio` with features `macros,rt-multi-thread`, then run the following program: > > ```rust > use casbin::prelude::*; > use casbin::CachedEnforcer; > use std::fs; > > #[tokio::main] > async fn main() { > fs::write("/tmp/model.conf", "[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = r.sub == p.sub && r.obj == p.obj && r.act == p.act\n").unwrap(); > fs::write("/tmp/policy.csv", "p, alice, data1, read\n").unwrap(); > let mut e = CachedEnforcer::new("/tmp/model.conf", "/tmp/policy.csv").await.unwrap(); > assert!(e.enforce(("alice", "data1", "read")).unwrap()); > > fs::write("/tmp/policy.csv", "").unwrap(); > e.load_policy().await.unwrap(); > assert!(e.get_policy().is_empty()); > > // Fails securely only if the stale allow was invalidated. > assert!(!e.enforce(("alice", "data1", "read")).unwrap()); > } > ``` > > On the affected code, the final assertion fails because `enforce(("alice", "data1", "read"))` remains `true` even though `get_policy()` is empty. The verification run also reproduced stale allows after `clear_policy()`, `set_model()`, and `enable_enforce(false)` followed by `enable_enforce(true)`; a control using `remove_policy()` correctly invalidated the cache. > > ## Fix suggestion > > Invalidate the wrapper's decision cache after every successful operation that can change authorization results, including the lifecycle delegations in `src/cached_enforcer.rs`. Centralizing this invariant in the wrapper is preferable to relying only on the five `InternalApi` mutation hooks, and enforcement state changes must not leave allows created while enforcement was disabled in the cache. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
