//! Adversarial IDOR & authorization tests. //! //! Focus: authorization and IDOR. //! Each test attempts to access, modify, or delete resources belonging to //! another user. Tests that PASS prove the app correctly rejects the attack. use crate::harness::TestHarness; use makenotwork::db; use serde_json::Value; /// Helper: create a "victim" creator with a published project, published item, /// and an unpublished blog post. Returns (victim_id, project_id, item_id, blog_post_id). /// Logs out when done. async fn setup_victim(h: &mut TestHarness) -> (db::UserId, String, String, String) { let victim_id = h.signup("victim", "victim@test.com", "password123").await; h.grant_creator(victim_id).await; h.client.post_form("/logout", "").await; h.login("victim", "password123").await; // Create project let resp = h .client .post_form("/api/projects", "slug=victim-shop&title=Victim+Shop") .await; assert_eq!(resp.status, 200, "Create project failed: {}", resp.text); let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap().to_string(); // Create item let resp = h .client .post_form( &format!("/api/projects/{project_id}/items"), "title=Secret+Item&item_type=digital&price_cents=1000", ) .await; assert_eq!(resp.status, 200, "Create item failed: {}", resp.text); let item: Value = resp.json(); let item_id = item["id"].as_str().unwrap().to_string(); // Publish both h.client .put_json( &format!("/api/projects/{project_id}"), r#"{"is_public": true}"#, ) .await; h.client .put_form(&format!("/api/items/{item_id}"), "is_public=true") .await; // Create blog post (unpublished, should not be visible to attacker) let resp = h .client .post_json( &format!("/api/projects/{project_id}/blog"), r#"{"title": "Draft Post"}"#, ) .await; assert_eq!(resp.status, 200, "Create blog post failed: {}", resp.text); let post: Value = resp.json(); let post_id = post["id"].as_str().unwrap().to_string(); h.client.post_form("/logout", "").await; (victim_id, project_id, item_id, post_id) } /// Helper: sign up an "attacker" creator and log in. /// Returns attacker's user_id. async fn setup_attacker(h: &mut TestHarness) -> db::UserId { let attacker_id = h .signup("attacker", "attacker@test.com", "password123") .await; h.grant_creator(attacker_id).await; h.client.post_form("/logout", "").await; h.login("attacker", "password123").await; attacker_id } // Project IDOR /// Vulnerability tested: IDOR on project update. /// Attacker knows victim's project UUID and tries to rename it. #[tokio::test] async fn project_update_by_non_owner() { let mut h = TestHarness::new().await; let (_victim_id, project_id, _item_id, _post_id) = setup_victim(&mut h).await; let _attacker_id = setup_attacker(&mut h).await; let resp = h .client .put_json( &format!("/api/projects/{project_id}"), r#"{"title": "Pwned"}"#, ) .await; assert_eq!( resp.status, 403, "Non-owner should not update another user's project: {} {}", resp.status, resp.text ); } /// Vulnerability tested: IDOR on project deletion. /// Attacker tries to delete victim's project. #[tokio::test] async fn project_delete_by_non_owner() { let mut h = TestHarness::new().await; let (_victim_id, project_id, _item_id, _post_id) = setup_victim(&mut h).await; let _attacker_id = setup_attacker(&mut h).await; let resp = h .client .delete(&format!("/api/projects/{project_id}")) .await; assert_eq!( resp.status, 403, "Non-owner should not delete another user's project: {} {}", resp.status, resp.text ); // Verify project still exists by logging back in as victim h.client.post_form("/logout", "").await; h.login("victim", "password123").await; let resp = h.client.get("/api/projects").await; let list: Value = resp.json(); let data = list["data"].as_array().unwrap(); assert_eq!(data.len(), 1, "Victim's project should still exist"); } // Item IDOR /// Vulnerability tested: IDOR on item creation. /// Attacker injects an item into victim's project. #[tokio::test] async fn item_create_in_others_project() { let mut h = TestHarness::new().await; let (_victim_id, project_id, _item_id, _post_id) = setup_victim(&mut h).await; let _attacker_id = setup_attacker(&mut h).await; let resp = h .client .post_form( &format!("/api/projects/{project_id}/items"), "title=Injected&item_type=digital&price_cents=0", ) .await; assert_eq!( resp.status, 403, "Non-owner should not create items in another user's project: {} {}", resp.status, resp.text ); } /// Vulnerability tested: IDOR on item update. /// Attacker tries to change victim's item price to $0. #[tokio::test] async fn item_update_by_non_owner() { let mut h = TestHarness::new().await; let (_victim_id, _project_id, item_id, _post_id) = setup_victim(&mut h).await; let _attacker_id = setup_attacker(&mut h).await; let resp = h .client .put_form( &format!("/api/items/{item_id}"), "price_cents=0&title=Free+Now", ) .await; assert_eq!( resp.status, 403, "Non-owner should not update another user's item: {} {}", resp.status, resp.text ); } /// Vulnerability tested: IDOR on item deletion. /// Attacker tries to delete victim's item. #[tokio::test] async fn item_delete_by_non_owner() { let mut h = TestHarness::new().await; let (_victim_id, _project_id, item_id, _post_id) = setup_victim(&mut h).await; let _attacker_id = setup_attacker(&mut h).await; let resp = h.client.delete(&format!("/api/items/{item_id}")).await; assert_eq!( resp.status, 403, "Non-owner should not delete another user's item: {} {}", resp.status, resp.text ); } /// Vulnerability tested: IDOR on item duplication. /// Attacker tries to clone victim's item into attacker's project. #[tokio::test] async fn item_duplicate_by_non_owner() { let mut h = TestHarness::new().await; let (_victim_id, _project_id, item_id, _post_id) = setup_victim(&mut h).await; let _attacker_id = setup_attacker(&mut h).await; let resp = h .client .post_form(&format!("/api/items/{item_id}/duplicate"), "") .await; assert_eq!( resp.status, 403, "Non-owner should not duplicate another user's item: {} {}", resp.status, resp.text ); } /// Vulnerability tested: IDOR on item tag manipulation. /// Attacker tries to add a tag to victim's item. #[tokio::test] async fn item_tags_by_non_owner() { let mut h = TestHarness::new().await; let (_victim_id, _project_id, item_id, _post_id) = setup_victim(&mut h).await; let _attacker_id = setup_attacker(&mut h).await; let fake_tag_id = uuid::Uuid::new_v4(); let resp = h .client .post_form( &format!("/api/items/{item_id}/tags"), &format!("tag_id={fake_tag_id}"), ) .await; assert_eq!( resp.status, 403, "Non-owner should not add tags to another user's item: {} {}", resp.status, resp.text ); } // Blog post IDOR /// Vulnerability tested: IDOR on blog post read (edit endpoint). /// Attacker tries to read victim's unpublished draft via the edit API. #[tokio::test] async fn blog_read_by_non_owner() { let mut h = TestHarness::new().await; let (_victim_id, _project_id, _item_id, post_id) = setup_victim(&mut h).await; let _attacker_id = setup_attacker(&mut h).await; let resp = h.client.get(&format!("/api/blog/{post_id}")).await; assert_eq!( resp.status, 403, "Non-owner should not read another user's blog post via edit API: {} {}", resp.status, resp.text ); } /// Vulnerability tested: IDOR on blog post update. /// Attacker tries to overwrite victim's blog post content. #[tokio::test] async fn blog_update_by_non_owner() { let mut h = TestHarness::new().await; let (_victim_id, _project_id, _item_id, post_id) = setup_victim(&mut h).await; let _attacker_id = setup_attacker(&mut h).await; let resp = h .client .put_json( &format!("/api/blog/{post_id}"), r#"{"title": "Defaced", "slug": "defaced", "body_markdown": "You got hacked", "is_published": true}"#, ) .await; assert_eq!( resp.status, 403, "Non-owner should not update another user's blog post: {} {}", resp.status, resp.text ); } /// Vulnerability tested: IDOR on blog post deletion. /// Attacker tries to delete victim's blog post. #[tokio::test] async fn blog_delete_by_non_owner() { let mut h = TestHarness::new().await; let (_victim_id, _project_id, _item_id, post_id) = setup_victim(&mut h).await; let _attacker_id = setup_attacker(&mut h).await; let resp = h.client.delete(&format!("/api/blog/{post_id}")).await; assert_eq!( resp.status, 403, "Non-owner should not delete another user's blog post: {} {}", resp.status, resp.text ); } // Permission boundary tests /// Vulnerability tested: Non-creator bypasses creator gate. /// Regular user (no creator permission) tries to create a project. #[tokio::test] async fn non_creator_create_project() { let mut h = TestHarness::new().await; // Sign up but do NOT grant creator let _user_id = h.signup("normie", "normie@test.com", "password123").await; let resp = h .client .post_form("/api/projects", "slug=my-shop&title=My+Shop") .await; assert_eq!( resp.status, 403, "Non-creator should not be able to create projects: {} {}", resp.status, resp.text ); } /// Vulnerability tested: Suspended creator bypasses suspension check. /// Creator is suspended, then tries to update their own project. #[tokio::test] async fn suspended_creator_blocked_from_writes() { let mut h = TestHarness::new().await; let user_id = h .signup("suspended", "suspended@test.com", "password123") .await; h.grant_creator(user_id).await; h.client.post_form("/logout", "").await; h.login("suspended", "password123").await; // Create project while not suspended let resp = h .client .post_form("/api/projects", "slug=my-project&title=My+Project") .await; assert_eq!( resp.status, 200, "Project creation should work: {}", resp.text ); let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap(); // Create item let resp = h .client .post_form( &format!("/api/projects/{project_id}/items"), "title=My+Item&item_type=digital&price_cents=500", ) .await; assert_eq!(resp.status, 200, "Item creation should work: {}", resp.text); let item: Value = resp.json(); let item_id = item["id"].as_str().unwrap(); // Suspend the user via direct DB db::users::suspend_user(&h.db, user_id, "test suspension") .await .unwrap(); // Re-login to pick up suspended state h.client.post_form("/logout", "").await; h.login("suspended", "password123").await; // Try to update project, should be blocked let resp = h .client .put_json( &format!("/api/projects/{project_id}"), r#"{"title": "Updated While Suspended"}"#, ) .await; assert_eq!( resp.status, 403, "Suspended user should not update projects: {} {}", resp.status, resp.text ); // Try to update item, should be blocked let resp = h .client .put_form( &format!("/api/items/{item_id}"), "title=Updated+While+Suspended", ) .await; assert_eq!( resp.status, 403, "Suspended user should not update items: {} {}", resp.status, resp.text ); // Try to create new project, should be blocked let resp = h .client .post_form("/api/projects", "slug=new-project&title=New+Project") .await; assert_eq!( resp.status, 403, "Suspended user should not create projects: {} {}", resp.status, resp.text ); } // Enumeration / information leakage /// Vulnerability tested: Resource enumeration via list endpoints. /// Attacker lists their own projects/items, victim's resources must not appear. #[tokio::test] async fn victim_resources_invisible_in_attacker_listing() { let mut h = TestHarness::new().await; let (_victim_id, _project_id, _item_id, _post_id) = setup_victim(&mut h).await; let _attacker_id = setup_attacker(&mut h).await; // List attacker's projects, should be empty (attacker has none) let resp = h.client.get("/api/projects").await; assert_eq!(resp.status, 200, "{}", resp.text); let list: Value = resp.json(); let data = list["data"].as_array().unwrap(); assert!( data.is_empty(), "Attacker's project list should not contain victim's projects, got {} items", data.len() ); // List attacker's promo codes, should be empty let resp = h.client.get("/api/promo-codes").await; assert_eq!(resp.status, 200, "{}", resp.text); let list: Value = resp.json(); let data = list["data"].as_array().unwrap(); assert!( data.is_empty(), "Attacker's promo code list should be empty" ); }