//! Adversarial IDOR & authorization tests. //! //! Focus: Authorization & IDOR (Option A from adversarial.md). //! 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!(resp.status.is_success(), "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/{}/items", project_id), "title=Secret+Item&item_type=digital&price_cents=1000", ) .await; assert!(resp.status.is_success(), "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/{}/blog", project_id), r#"{"title": "Draft Post"}"#, ) .await; assert!(resp.status.is_success(), "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/{}/items", project_id), "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/{}/duplicate", item_id), "") .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/{}/tags", item_id), &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!(resp.status.is_success(), "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/{}/items", project_id), "title=My+Item&item_type=digital&price_cents=500", ) .await; assert!(resp.status.is_success(), "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!(resp.status.is_success()); 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!(resp.status.is_success()); let list: Value = resp.json(); let data = list["data"].as_array().unwrap(); assert!( data.is_empty(), "Attacker's promo code list should be empty" ); }