Skip to main content

max / makenotwork

13.5 KB · 429 lines History Blame Raw
1 //! Adversarial IDOR & authorization tests.
2 //!
3 //! Focus: authorization and IDOR.
4 //! Each test attempts to access, modify, or delete resources belonging to
5 //! another user. Tests that PASS prove the app correctly rejects the attack.
6
7 use crate::harness::TestHarness;
8 use makenotwork::db;
9 use serde_json::Value;
10
11 /// Helper: create a "victim" creator with a published project, published item,
12 /// and an unpublished blog post. Returns (victim_id, project_id, item_id, blog_post_id).
13 /// Logs out when done.
14 async fn setup_victim(h: &mut TestHarness) -> (db::UserId, String, String, String) {
15 let victim_id = h.signup("victim", "victim@test.com", "password123").await;
16 h.grant_creator(victim_id).await;
17 h.client.post_form("/logout", "").await;
18 h.login("victim", "password123").await;
19
20 // Create project
21 let resp = h
22 .client
23 .post_form("/api/projects", "slug=victim-shop&title=Victim+Shop")
24 .await;
25 assert_eq!(resp.status, 200, "Create project failed: {}", resp.text);
26 let project: Value = resp.json();
27 let project_id = project["id"].as_str().unwrap().to_string();
28
29 // Create item
30 let resp = h
31 .client
32 .post_form(
33 &format!("/api/projects/{project_id}/items"),
34 "title=Secret+Item&item_type=digital&price_cents=1000",
35 )
36 .await;
37 assert_eq!(resp.status, 200, "Create item failed: {}", resp.text);
38 let item: Value = resp.json();
39 let item_id = item["id"].as_str().unwrap().to_string();
40
41 // Publish both
42 h.client
43 .put_json(
44 &format!("/api/projects/{project_id}"),
45 r#"{"is_public": true}"#,
46 )
47 .await;
48 h.client
49 .put_form(&format!("/api/items/{item_id}"), "is_public=true")
50 .await;
51
52 // Create blog post (unpublished, should not be visible to attacker)
53 let resp = h
54 .client
55 .post_json(
56 &format!("/api/projects/{project_id}/blog"),
57 r#"{"title": "Draft Post"}"#,
58 )
59 .await;
60 assert_eq!(resp.status, 200, "Create blog post failed: {}", resp.text);
61 let post: Value = resp.json();
62 let post_id = post["id"].as_str().unwrap().to_string();
63
64 h.client.post_form("/logout", "").await;
65 (victim_id, project_id, item_id, post_id)
66 }
67
68 /// Helper: sign up an "attacker" creator and log in.
69 /// Returns attacker's user_id.
70 async fn setup_attacker(h: &mut TestHarness) -> db::UserId {
71 let attacker_id = h
72 .signup("attacker", "attacker@test.com", "password123")
73 .await;
74 h.grant_creator(attacker_id).await;
75 h.client.post_form("/logout", "").await;
76 h.login("attacker", "password123").await;
77 attacker_id
78 }
79
80 // Project IDOR
81
82 /// Vulnerability tested: IDOR on project update.
83 /// Attacker knows victim's project UUID and tries to rename it.
84 #[tokio::test]
85 async fn project_update_by_non_owner() {
86 let mut h = TestHarness::new().await;
87 let (_victim_id, project_id, _item_id, _post_id) = setup_victim(&mut h).await;
88 let _attacker_id = setup_attacker(&mut h).await;
89
90 let resp = h
91 .client
92 .put_json(
93 &format!("/api/projects/{project_id}"),
94 r#"{"title": "Pwned"}"#,
95 )
96 .await;
97 assert_eq!(
98 resp.status, 403,
99 "Non-owner should not update another user's project: {} {}",
100 resp.status, resp.text
101 );
102 }
103
104 /// Vulnerability tested: IDOR on project deletion.
105 /// Attacker tries to delete victim's project.
106 #[tokio::test]
107 async fn project_delete_by_non_owner() {
108 let mut h = TestHarness::new().await;
109 let (_victim_id, project_id, _item_id, _post_id) = setup_victim(&mut h).await;
110 let _attacker_id = setup_attacker(&mut h).await;
111
112 let resp = h
113 .client
114 .delete(&format!("/api/projects/{project_id}"))
115 .await;
116 assert_eq!(
117 resp.status, 403,
118 "Non-owner should not delete another user's project: {} {}",
119 resp.status, resp.text
120 );
121
122 // Verify project still exists by logging back in as victim
123 h.client.post_form("/logout", "").await;
124 h.login("victim", "password123").await;
125 let resp = h.client.get("/api/projects").await;
126 let list: Value = resp.json();
127 let data = list["data"].as_array().unwrap();
128 assert_eq!(data.len(), 1, "Victim's project should still exist");
129 }
130
131 // Item IDOR
132
133 /// Vulnerability tested: IDOR on item creation.
134 /// Attacker injects an item into victim's project.
135 #[tokio::test]
136 async fn item_create_in_others_project() {
137 let mut h = TestHarness::new().await;
138 let (_victim_id, project_id, _item_id, _post_id) = setup_victim(&mut h).await;
139 let _attacker_id = setup_attacker(&mut h).await;
140
141 let resp = h
142 .client
143 .post_form(
144 &format!("/api/projects/{project_id}/items"),
145 "title=Injected&item_type=digital&price_cents=0",
146 )
147 .await;
148 assert_eq!(
149 resp.status, 403,
150 "Non-owner should not create items in another user's project: {} {}",
151 resp.status, resp.text
152 );
153 }
154
155 /// Vulnerability tested: IDOR on item update.
156 /// Attacker tries to change victim's item price to $0.
157 #[tokio::test]
158 async fn item_update_by_non_owner() {
159 let mut h = TestHarness::new().await;
160 let (_victim_id, _project_id, item_id, _post_id) = setup_victim(&mut h).await;
161 let _attacker_id = setup_attacker(&mut h).await;
162
163 let resp = h
164 .client
165 .put_form(
166 &format!("/api/items/{item_id}"),
167 "price_cents=0&title=Free+Now",
168 )
169 .await;
170 assert_eq!(
171 resp.status, 403,
172 "Non-owner should not update another user's item: {} {}",
173 resp.status, resp.text
174 );
175 }
176
177 /// Vulnerability tested: IDOR on item deletion.
178 /// Attacker tries to delete victim's item.
179 #[tokio::test]
180 async fn item_delete_by_non_owner() {
181 let mut h = TestHarness::new().await;
182 let (_victim_id, _project_id, item_id, _post_id) = setup_victim(&mut h).await;
183 let _attacker_id = setup_attacker(&mut h).await;
184
185 let resp = h.client.delete(&format!("/api/items/{item_id}")).await;
186 assert_eq!(
187 resp.status, 403,
188 "Non-owner should not delete another user's item: {} {}",
189 resp.status, resp.text
190 );
191 }
192
193 /// Vulnerability tested: IDOR on item duplication.
194 /// Attacker tries to clone victim's item into attacker's project.
195 #[tokio::test]
196 async fn item_duplicate_by_non_owner() {
197 let mut h = TestHarness::new().await;
198 let (_victim_id, _project_id, item_id, _post_id) = setup_victim(&mut h).await;
199 let _attacker_id = setup_attacker(&mut h).await;
200
201 let resp = h
202 .client
203 .post_form(&format!("/api/items/{item_id}/duplicate"), "")
204 .await;
205 assert_eq!(
206 resp.status, 403,
207 "Non-owner should not duplicate another user's item: {} {}",
208 resp.status, resp.text
209 );
210 }
211
212 /// Vulnerability tested: IDOR on item tag manipulation.
213 /// Attacker tries to add a tag to victim's item.
214 #[tokio::test]
215 async fn item_tags_by_non_owner() {
216 let mut h = TestHarness::new().await;
217 let (_victim_id, _project_id, item_id, _post_id) = setup_victim(&mut h).await;
218 let _attacker_id = setup_attacker(&mut h).await;
219
220 let fake_tag_id = uuid::Uuid::new_v4();
221 let resp = h
222 .client
223 .post_form(
224 &format!("/api/items/{item_id}/tags"),
225 &format!("tag_id={fake_tag_id}"),
226 )
227 .await;
228 assert_eq!(
229 resp.status, 403,
230 "Non-owner should not add tags to another user's item: {} {}",
231 resp.status, resp.text
232 );
233 }
234
235 // Blog post IDOR
236
237 /// Vulnerability tested: IDOR on blog post read (edit endpoint).
238 /// Attacker tries to read victim's unpublished draft via the edit API.
239 #[tokio::test]
240 async fn blog_read_by_non_owner() {
241 let mut h = TestHarness::new().await;
242 let (_victim_id, _project_id, _item_id, post_id) = setup_victim(&mut h).await;
243 let _attacker_id = setup_attacker(&mut h).await;
244
245 let resp = h.client.get(&format!("/api/blog/{post_id}")).await;
246 assert_eq!(
247 resp.status, 403,
248 "Non-owner should not read another user's blog post via edit API: {} {}",
249 resp.status, resp.text
250 );
251 }
252
253 /// Vulnerability tested: IDOR on blog post update.
254 /// Attacker tries to overwrite victim's blog post content.
255 #[tokio::test]
256 async fn blog_update_by_non_owner() {
257 let mut h = TestHarness::new().await;
258 let (_victim_id, _project_id, _item_id, post_id) = setup_victim(&mut h).await;
259 let _attacker_id = setup_attacker(&mut h).await;
260
261 let resp = h
262 .client
263 .put_json(
264 &format!("/api/blog/{post_id}"),
265 r#"{"title": "Defaced", "slug": "defaced", "body_markdown": "You got hacked", "is_published": true}"#,
266 )
267 .await;
268 assert_eq!(
269 resp.status, 403,
270 "Non-owner should not update another user's blog post: {} {}",
271 resp.status, resp.text
272 );
273 }
274
275 /// Vulnerability tested: IDOR on blog post deletion.
276 /// Attacker tries to delete victim's blog post.
277 #[tokio::test]
278 async fn blog_delete_by_non_owner() {
279 let mut h = TestHarness::new().await;
280 let (_victim_id, _project_id, _item_id, post_id) = setup_victim(&mut h).await;
281 let _attacker_id = setup_attacker(&mut h).await;
282
283 let resp = h.client.delete(&format!("/api/blog/{post_id}")).await;
284 assert_eq!(
285 resp.status, 403,
286 "Non-owner should not delete another user's blog post: {} {}",
287 resp.status, resp.text
288 );
289 }
290
291 // Permission boundary tests
292
293 /// Vulnerability tested: Non-creator bypasses creator gate.
294 /// Regular user (no creator permission) tries to create a project.
295 #[tokio::test]
296 async fn non_creator_create_project() {
297 let mut h = TestHarness::new().await;
298 // Sign up but do NOT grant creator
299 let _user_id = h.signup("normie", "normie@test.com", "password123").await;
300
301 let resp = h
302 .client
303 .post_form("/api/projects", "slug=my-shop&title=My+Shop")
304 .await;
305 assert_eq!(
306 resp.status, 403,
307 "Non-creator should not be able to create projects: {} {}",
308 resp.status, resp.text
309 );
310 }
311
312 /// Vulnerability tested: Suspended creator bypasses suspension check.
313 /// Creator is suspended, then tries to update their own project.
314 #[tokio::test]
315 async fn suspended_creator_blocked_from_writes() {
316 let mut h = TestHarness::new().await;
317 let user_id = h
318 .signup("suspended", "suspended@test.com", "password123")
319 .await;
320 h.grant_creator(user_id).await;
321 h.client.post_form("/logout", "").await;
322 h.login("suspended", "password123").await;
323
324 // Create project while not suspended
325 let resp = h
326 .client
327 .post_form("/api/projects", "slug=my-project&title=My+Project")
328 .await;
329 assert_eq!(
330 resp.status, 200,
331 "Project creation should work: {}",
332 resp.text
333 );
334 let project: Value = resp.json();
335 let project_id = project["id"].as_str().unwrap();
336
337 // Create item
338 let resp = h
339 .client
340 .post_form(
341 &format!("/api/projects/{project_id}/items"),
342 "title=My+Item&item_type=digital&price_cents=500",
343 )
344 .await;
345 assert_eq!(resp.status, 200, "Item creation should work: {}", resp.text);
346 let item: Value = resp.json();
347 let item_id = item["id"].as_str().unwrap();
348
349 // Suspend the user via direct DB
350 db::users::suspend_user(&h.db, user_id, "test suspension")
351 .await
352 .unwrap();
353
354 // Re-login to pick up suspended state
355 h.client.post_form("/logout", "").await;
356 h.login("suspended", "password123").await;
357
358 // Try to update project, should be blocked
359 let resp = h
360 .client
361 .put_json(
362 &format!("/api/projects/{project_id}"),
363 r#"{"title": "Updated While Suspended"}"#,
364 )
365 .await;
366 assert_eq!(
367 resp.status, 403,
368 "Suspended user should not update projects: {} {}",
369 resp.status, resp.text
370 );
371
372 // Try to update item, should be blocked
373 let resp = h
374 .client
375 .put_form(
376 &format!("/api/items/{item_id}"),
377 "title=Updated+While+Suspended",
378 )
379 .await;
380 assert_eq!(
381 resp.status, 403,
382 "Suspended user should not update items: {} {}",
383 resp.status, resp.text
384 );
385
386 // Try to create new project, should be blocked
387 let resp = h
388 .client
389 .post_form("/api/projects", "slug=new-project&title=New+Project")
390 .await;
391 assert_eq!(
392 resp.status, 403,
393 "Suspended user should not create projects: {} {}",
394 resp.status, resp.text
395 );
396 }
397
398 // Enumeration / information leakage
399
400 /// Vulnerability tested: Resource enumeration via list endpoints.
401 /// Attacker lists their own projects/items, victim's resources must not appear.
402 #[tokio::test]
403 async fn victim_resources_invisible_in_attacker_listing() {
404 let mut h = TestHarness::new().await;
405 let (_victim_id, _project_id, _item_id, _post_id) = setup_victim(&mut h).await;
406 let _attacker_id = setup_attacker(&mut h).await;
407
408 // List attacker's projects, should be empty (attacker has none)
409 let resp = h.client.get("/api/projects").await;
410 assert_eq!(resp.status, 200, "{}", resp.text);
411 let list: Value = resp.json();
412 let data = list["data"].as_array().unwrap();
413 assert!(
414 data.is_empty(),
415 "Attacker's project list should not contain victim's projects, got {} items",
416 data.len()
417 );
418
419 // List attacker's promo codes, should be empty
420 let resp = h.client.get("/api/promo-codes").await;
421 assert_eq!(resp.status, 200, "{}", resp.text);
422 let list: Value = resp.json();
423 let data = list["data"].as_array().unwrap();
424 assert!(
425 data.is_empty(),
426 "Attacker's promo code list should be empty"
427 );
428 }
429