lonkero 3.7.0

Web scanner built for actual pentests. Fast, modular, Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
// Copyright (c) 2026 Bountyy Oy. All rights reserved.
// This software is proprietary and confidential.

use crate::http_client::HttpClient;
use crate::types::{ScanConfig, Severity, Vulnerability};
use std::sync::Arc;
use tracing::{debug, info};

mod uuid {
    pub use uuid::Uuid;
}

/// Scanner for file upload vulnerabilities
pub struct FileUploadVulnerabilitiesScanner {
    http_client: Arc<HttpClient>,
    test_marker: String,
}

impl FileUploadVulnerabilitiesScanner {
    pub fn new(http_client: Arc<HttpClient>) -> Self {
        let test_marker = format!(
            "upload-{}",
            uuid::Uuid::new_v4().to_string().replace("-", "")
        );
        Self {
            http_client,
            test_marker,
        }
    }

    /// Run file upload vulnerabilities scan
    pub async fn scan(
        &self,
        url: &str,
        _config: &ScanConfig,
    ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
        info!("Starting file upload vulnerabilities scan on {}", url);

        let mut all_vulnerabilities = Vec::new();
        let mut total_tests = 0;

        // Step 1: Discover upload endpoints dynamically
        total_tests += 1;
        let upload_endpoints = self.discover_upload_endpoints(url).await?;

        if upload_endpoints.is_empty() {
            info!("No upload endpoints found, testing default endpoints");
            // Fallback to testing common endpoints
            let (vulns, tests) = self.test_default_endpoints(url).await?;
            all_vulnerabilities.extend(vulns);
            total_tests += tests;
        } else {
            info!("Found {} upload endpoints to test", upload_endpoints.len());

            // Test each discovered endpoint
            for endpoint in upload_endpoints {
                if !all_vulnerabilities.is_empty() {
                    break; // Found vulnerability, stop testing
                }

                // Test unrestricted file extensions
                let (vulns, tests) = self.test_unrestricted_extensions(&endpoint).await?;
                all_vulnerabilities.extend(vulns);
                total_tests += tests;

                if all_vulnerabilities.is_empty() {
                    // Test MIME type bypass
                    let (vulns, tests) = self.test_mime_type_bypass(&endpoint).await?;
                    all_vulnerabilities.extend(vulns);
                    total_tests += tests;
                }

                if all_vulnerabilities.is_empty() {
                    // Test path traversal in filename
                    let (vulns, tests) = self.test_path_traversal(&endpoint).await?;
                    all_vulnerabilities.extend(vulns);
                    total_tests += tests;
                }

                if all_vulnerabilities.is_empty() {
                    // Test double extension bypass
                    let (vulns, tests) = self.test_double_extension(&endpoint).await?;
                    all_vulnerabilities.extend(vulns);
                    total_tests += tests;
                }
            }
        }

        info!(
            "File upload vulnerabilities scan completed: {} tests run, {} vulnerabilities found",
            total_tests,
            all_vulnerabilities.len()
        );

        Ok((all_vulnerabilities, total_tests))
    }

    /// Discover upload endpoints by scanning for forms with enctype="multipart/form-data"
    async fn discover_upload_endpoints(&self, url: &str) -> anyhow::Result<Vec<String>> {
        let mut endpoints = Vec::new();

        match self.http_client.get(url).await {
            Ok(response) => {
                let body = response.body.to_lowercase();

                // Look for forms with multipart/form-data
                if body.contains("multipart/form-data") {
                    // Extract form actions
                    let form_regex = regex::Regex::new(
                        r#"<form[^>]*action=["']([^"']+)["'][^>]*>[\s\S]*?multipart/form-data"#,
                    )
                    .ok();
                    let form_regex2 = regex::Regex::new(
                        r#"multipart/form-data[\s\S]*?<form[^>]*action=["']([^"']+)["']"#,
                    )
                    .ok();

                    let response_body = &response.body;

                    if let Some(re) = form_regex {
                        for cap in re.captures_iter(response_body) {
                            if let Some(action) = cap.get(1) {
                                let endpoint = self.normalize_endpoint(url, action.as_str());
                                if !endpoints.contains(&endpoint) {
                                    endpoints.push(endpoint);
                                }
                            }
                        }
                    }

                    if let Some(re) = form_regex2 {
                        for cap in re.captures_iter(response_body) {
                            if let Some(action) = cap.get(1) {
                                let endpoint = self.normalize_endpoint(url, action.as_str());
                                if !endpoints.contains(&endpoint) {
                                    endpoints.push(endpoint);
                                }
                            }
                        }
                    }
                }

                // Also check for common upload API patterns in JavaScript
                let js_patterns = [
                    r#"/upload"#,
                    r#"/api/upload"#,
                    r#"/file/upload"#,
                    r#"/files/upload"#,
                ];

                for pattern in js_patterns {
                    if response.body.contains(pattern) {
                        let endpoint = self.normalize_endpoint(url, pattern);
                        if !endpoints.contains(&endpoint) {
                            endpoints.push(endpoint);
                        }
                    }
                }
            }
            Err(e) => {
                info!("Failed to fetch page for endpoint discovery: {}", e);
            }
        }

        Ok(endpoints)
    }

    /// Normalize endpoint URL
    fn normalize_endpoint(&self, base_url: &str, endpoint: &str) -> String {
        if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
            endpoint.to_string()
        } else if endpoint.starts_with('/') {
            // Extract base URL
            if let Ok(parsed) = url::Url::parse(base_url) {
                format!(
                    "{}://{}{}",
                    parsed.scheme(),
                    parsed.host_str().unwrap_or(""),
                    endpoint
                )
            } else {
                format!("{}{}", base_url.trim_end_matches('/'), endpoint)
            }
        } else {
            format!("{}/{}", base_url.trim_end_matches('/'), endpoint)
        }
    }

    /// Test default endpoints when no upload forms are found
    async fn test_default_endpoints(
        &self,
        url: &str,
    ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
        let mut vulnerabilities = Vec::new();
        let tests_run = 3;

        let endpoints = vec![
            format!("{}/upload", url.trim_end_matches('/')),
            format!("{}/api/upload", url.trim_end_matches('/')),
            format!("{}/file/upload", url.trim_end_matches('/')),
        ];

        for endpoint in &endpoints {
            let (vulns, _) = self.test_unrestricted_extensions(endpoint).await?;
            vulnerabilities.extend(vulns);
            if !vulnerabilities.is_empty() {
                break;
            }
        }

        Ok((vulnerabilities, tests_run))
    }

    /// Test unrestricted file extensions
    async fn test_unrestricted_extensions(
        &self,
        url: &str,
    ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
        let mut vulnerabilities = Vec::new();
        let tests_run = 5;

        debug!("Testing unrestricted file extensions on: {}", url);

        // Dangerous file extensions with unique markers for verification
        let dangerous_extensions = vec![
            ("php", format!("<?php echo '{}'; ?>", self.test_marker)),
            (
                "jsp",
                format!("<% out.println(\"{}\"); %>", self.test_marker),
            ),
            (
                "asp",
                format!("<% Response.Write(\"{}\") %>", self.test_marker),
            ),
            (
                "aspx",
                format!(
                    "<%@ Page Language=\"C#\" %><% Response.Write(\"{}\"); %>",
                    self.test_marker
                ),
            ),
            ("sh", format!("#!/bin/bash\necho {}", self.test_marker)),
        ];

        for (ext, content) in &dangerous_extensions {
            let filename = format!("{}.{}", self.test_marker, ext);
            let boundary = format!(
                "----WebKitFormBoundary{}",
                uuid::Uuid::new_v4().to_string().replace("-", "")
            );

            let body = self.create_multipart_body(
                &boundary,
                &filename,
                content,
                "application/octet-stream",
            );
            let headers = vec![(
                "Content-Type".to_string(),
                format!("multipart/form-data; boundary={}", boundary),
            )];

            // Step 1: Upload the file
            match self
                .http_client
                .post_with_headers(url, &body, headers)
                .await
            {
                Ok(response) => {
                    // Step 2: Extract upload path from response or try common paths
                    let upload_paths = self.extract_upload_paths(&response.body, &filename, url);

                    // Step 3: Verify file was uploaded and can be accessed
                    for upload_path in upload_paths {
                        match self.http_client.get(&upload_path).await {
                            Ok(verify_response) => {
                                // Step 4: Check if our marker is in the response (proof of execution)
                                if verify_response.body.contains(&self.test_marker) {
                                    info!(
                                        "VERIFIED: File uploaded and executed at {}",
                                        upload_path
                                    );
                                    vulnerabilities.push(self.create_vulnerability(
                                        "Unrestricted File Upload with Code Execution",
                                        url,
                                        &format!("Uploaded {} and verified execution. File accessible at: {}. Marker '{}' found in response.", filename, upload_path, self.test_marker),
                                        Severity::Critical,
                                        "CWE-434",
                                    ));
                                    return Ok((vulnerabilities, tests_run));
                                } else if verify_response.status_code == 200 {
                                    // Check for soft 404 - server returns 200 but body shows error
                                    let body_lower = verify_response.body.to_lowercase();

                                    // SPAs often return main page HTML even for missing files
                                    let is_spa_page = body_lower.contains("<!doctype html")
                                        || body_lower.contains("<div id=\"app\"")
                                        || body_lower.contains("<div id=\"q-app\"")
                                        || body_lower.contains("<div id=\"root\"")
                                        || (body_lower.contains("<html")
                                            && !body_lower
                                                .contains(&self.test_marker.to_lowercase()));

                                    // JSON error responses
                                    let is_json_error = (body_lower.starts_with("{")
                                        || body_lower.starts_with("["))
                                        && (body_lower.contains("\"error\"")
                                            || body_lower.contains("\"message\"")
                                            || body_lower.contains("not found")
                                            || body_lower.contains("\"status\":404")
                                            || body_lower.contains("\"code\":404"));

                                    // Soft 404 detection: SPAs return 200 for everything,
                                    // so check for not-found content in the body
                                    let has_not_found_text =
                                        body_lower.contains("file not found") ||
                                        body_lower.contains("page not found") ||
                                        body_lower.contains("resource not found") ||
                                        body_lower.contains("cannot be found") ||
                                        body_lower.contains("does not exist") ||
                                        body_lower.contains("no such file") ||
                                        // Multi-language 404 patterns
                                        body_lower.contains("sivua ei löydy") ||  // Finnish
                                        body_lower.contains("sivu ei löytynyt") ||  // Finnish variant
                                        body_lower.contains("ei löydy") ||  // Finnish generic "not found"
                                        body_lower.contains("seite nicht gefunden") ||  // German
                                        body_lower.contains("página no encontrada") ||  // Spanish
                                        body_lower.contains("page introuvable") ||  // French
                                        body_lower.contains("pagina niet gevonden");  // Dutch

                                    // SPA pages return the app shell HTML for all routes
                                    // Only treat as soft-404 if body doesn't contain our test marker
                                    let is_spa_soft_404 = is_spa_page
                                        && !body_lower.contains(&self.test_marker.to_lowercase());

                                    let is_soft_404 = has_not_found_text ||
                                        is_spa_soft_404 ||
                                        is_json_error;

                                    if is_soft_404 {
                                        // This is a soft 404 - file doesn't actually exist
                                        info!("[FileUpload] Soft 404 detected at {} - not a real file", upload_path);
                                        continue;
                                    }

                                    // File exists but didn't execute - still a vulnerability but lower severity
                                    info!("File uploaded but not executed at {}", upload_path);
                                    vulnerabilities.push(self.create_vulnerability(
                                        "Unrestricted File Upload",
                                        url,
                                        &format!("Uploaded {} to {}. File is accessible but execution not confirmed.", filename, upload_path),
                                        Severity::High,
                                        "CWE-434",
                                    ));
                                    return Ok((vulnerabilities, tests_run));
                                }
                            }
                            Err(_) => {
                                // File not accessible at this path, try next one
                                continue;
                            }
                        }
                    }
                }
                Err(e) => {
                    info!("Upload test failed for .{}: {}", ext, e);
                }
            }
        }

        Ok((vulnerabilities, tests_run))
    }

    /// Extract potential upload paths from response or construct common ones
    fn extract_upload_paths(
        &self,
        response_body: &str,
        filename: &str,
        base_url: &str,
    ) -> Vec<String> {
        let mut paths = Vec::new();

        // Try to extract path from response JSON
        if let Ok(json) = serde_json::from_str::<serde_json::Value>(response_body) {
            // Check common JSON fields for upload path
            if let Some(path) = json.get("path").and_then(|v| v.as_str()) {
                paths.push(self.normalize_endpoint(base_url, path));
            }
            if let Some(url) = json.get("url").and_then(|v| v.as_str()) {
                paths.push(self.normalize_endpoint(base_url, url));
            }
            if let Some(location) = json.get("location").and_then(|v| v.as_str()) {
                paths.push(self.normalize_endpoint(base_url, location));
            }
            if let Some(file) = json.get("file").and_then(|v| v.as_str()) {
                paths.push(self.normalize_endpoint(base_url, file));
            }
        }

        // Try to extract from response body using regex
        let url_pattern = regex::Regex::new(&format!(
            r#"["'](/[^"']*{}[^"']*)["']"#,
            regex::escape(filename)
        ))
        .ok();
        if let Some(re) = url_pattern {
            for cap in re.captures_iter(response_body) {
                if let Some(path) = cap.get(1) {
                    paths.push(self.normalize_endpoint(base_url, path.as_str()));
                }
            }
        }

        // Try common upload directories
        let common_paths = vec![
            format!("/uploads/{}", filename),
            format!("/upload/{}", filename),
            format!("/files/{}", filename),
            format!("/static/uploads/{}", filename),
            format!("/media/{}", filename),
            format!("/content/{}", filename),
        ];

        for path in common_paths {
            paths.push(self.normalize_endpoint(base_url, &path));
        }

        paths
    }

    /// Test MIME type validation bypass
    async fn test_mime_type_bypass(
        &self,
        url: &str,
    ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
        let mut vulnerabilities = Vec::new();
        let tests_run = 3;

        debug!("Testing MIME type validation bypass on: {}", url);

        // Upload PHP file with image MIME type
        let payloads = vec![
            (
                "php",
                format!("<?php echo '{}'; ?>", self.test_marker),
                "image/jpeg",
                "MIME type spoofing with image/jpeg",
            ),
            (
                "php",
                format!("<?php echo '{}'; ?>", self.test_marker),
                "image/png",
                "MIME type spoofing with image/png",
            ),
            (
                "jsp",
                format!("<% out.println(\"{}\"); %>", self.test_marker),
                "image/gif",
                "MIME type spoofing with image/gif",
            ),
        ];

        for (ext, content, mime_type, description) in &payloads {
            let filename = format!("{}.{}", self.test_marker, ext);
            let boundary = format!(
                "----WebKitFormBoundary{}",
                uuid::Uuid::new_v4().to_string().replace("-", "")
            );

            let body = self.create_multipart_body(&boundary, &filename, content, mime_type);
            let headers = vec![(
                "Content-Type".to_string(),
                format!("multipart/form-data; boundary={}", boundary),
            )];

            match self
                .http_client
                .post_with_headers(url, &body, headers)
                .await
            {
                Ok(response) => {
                    let upload_paths = self.extract_upload_paths(&response.body, &filename, url);

                    for upload_path in upload_paths {
                        match self.http_client.get(&upload_path).await {
                            Ok(verify_response) => {
                                if verify_response.body.contains(&self.test_marker) {
                                    info!(
                                        "VERIFIED: MIME bypass successful, file executed at {}",
                                        upload_path
                                    );
                                    vulnerabilities.push(self.create_vulnerability(
                                        "File Upload MIME Type Bypass with Code Execution",
                                        url,
                                        &format!("{}: Uploaded {} as {} and verified execution at {}. Marker found in response.", description, filename, mime_type, upload_path),
                                        Severity::Critical,
                                        "CWE-434",
                                    ));
                                    return Ok((vulnerabilities, tests_run));
                                } else if verify_response.status_code == 200 {
                                    vulnerabilities.push(self.create_vulnerability(
                                        "File Upload MIME Type Bypass",
                                        url,
                                        &format!("{}: Uploaded {} as {} to {}. File accessible but execution not confirmed.", description, filename, mime_type, upload_path),
                                        Severity::High,
                                        "CWE-434",
                                    ));
                                    return Ok((vulnerabilities, tests_run));
                                }
                            }
                            Err(_) => continue,
                        }
                    }
                }
                Err(e) => {
                    info!("MIME type bypass test failed: {}", e);
                }
            }
        }

        Ok((vulnerabilities, tests_run))
    }

    /// Test path traversal in filename
    async fn test_path_traversal(&self, url: &str) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
        let mut vulnerabilities = Vec::new();
        let tests_run = 3;

        debug!("Testing path traversal in file upload on: {}", url);

        // Path traversal filenames with unique content
        let traversal_filenames = vec![
            format!("../../../tmp/{}.txt", self.test_marker),
            format!("..\\..\\..\\tmp\\{}.txt", self.test_marker),
            format!("....//....//tmp/{}.txt", self.test_marker),
        ];

        for filename in &traversal_filenames {
            let boundary = format!(
                "----WebKitFormBoundary{}",
                uuid::Uuid::new_v4().to_string().replace("-", "")
            );
            let content = format!("path_traversal_{}", self.test_marker);

            let body = self.create_multipart_body(&boundary, filename, &content, "text/plain");
            let headers = vec![(
                "Content-Type".to_string(),
                format!("multipart/form-data; boundary={}", boundary),
            )];

            match self
                .http_client
                .post_with_headers(url, &body, headers)
                .await
            {
                Ok(response) => {
                    // Check if upload succeeded (status 200/201 and no error messages)
                    if (response.status_code == 200 || response.status_code == 201)
                        && !response.body.to_lowercase().contains("invalid")
                        && !response.body.to_lowercase().contains("error")
                        && !response.body.to_lowercase().contains("forbidden")
                    {
                        // Try to access the file in traversed location
                        let traversed_filename = format!("{}.txt", self.test_marker);
                        let potential_paths = vec![
                            format!("/tmp/{}", traversed_filename),
                            self.normalize_endpoint(
                                url,
                                &format!("/../../../tmp/{}", traversed_filename),
                            ),
                        ];

                        for path in potential_paths {
                            if let Ok(verify_response) = self.http_client.get(&path).await {
                                if verify_response.body.contains(&content) {
                                    info!(
                                        "VERIFIED: Path traversal successful, file found at {}",
                                        path
                                    );
                                    vulnerabilities.push(self.create_vulnerability(
                                        "File Upload Path Traversal",
                                        url,
                                        &format!("Uploaded file with path traversal filename '{}' and verified at {}. Content marker found.", filename, path),
                                        Severity::High,
                                        "CWE-22",
                                    ));
                                    return Ok((vulnerabilities, tests_run));
                                }
                            }
                        }

                        // Even if we can't verify the file location, accepting path traversal is a vulnerability
                        vulnerabilities.push(self.create_vulnerability(
                            "File Upload Path Traversal (Unverified)",
                            url,
                            &format!("Server accepted path traversal filename '{}' without error. File location could not be verified.", filename),
                            Severity::Medium,
                            "CWE-22",
                        ));
                        return Ok((vulnerabilities, tests_run));
                    }
                }
                Err(e) => {
                    info!("Path traversal test failed: {}", e);
                }
            }
        }

        Ok((vulnerabilities, tests_run))
    }

    /// Test double extension bypass
    async fn test_double_extension(
        &self,
        url: &str,
    ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
        let mut vulnerabilities = Vec::new();
        let tests_run = 3;

        debug!("Testing double extension bypass on: {}", url);

        // Double extension filenames with unique markers
        let double_extensions = vec![
            (
                format!("{}.php.jpg", self.test_marker),
                format!("<?php echo '{}'; ?>", self.test_marker),
            ),
            (
                format!("{}.jsp.png", self.test_marker),
                format!("<% out.println(\"{}\"); %>", self.test_marker),
            ),
            (
                format!("{}.php.gif", self.test_marker),
                format!("<?php echo '{}'; ?>", self.test_marker),
            ),
        ];

        for (filename, content) in &double_extensions {
            let boundary = format!(
                "----WebKitFormBoundary{}",
                uuid::Uuid::new_v4().to_string().replace("-", "")
            );

            let body = self.create_multipart_body(&boundary, filename, content, "image/jpeg");
            let headers = vec![(
                "Content-Type".to_string(),
                format!("multipart/form-data; boundary={}", boundary),
            )];

            match self
                .http_client
                .post_with_headers(url, &body, headers)
                .await
            {
                Ok(response) => {
                    let upload_paths = self.extract_upload_paths(&response.body, filename, url);

                    for upload_path in upload_paths {
                        match self.http_client.get(&upload_path).await {
                            Ok(verify_response) => {
                                if verify_response.body.contains(&self.test_marker) {
                                    info!("VERIFIED: Double extension bypass successful, file executed at {}", upload_path);
                                    vulnerabilities.push(self.create_vulnerability(
                                        "File Upload Double Extension Bypass with Code Execution",
                                        url,
                                        &format!("Uploaded double extension file '{}' and verified execution at {}. Marker found in response.", filename, upload_path),
                                        Severity::Critical,
                                        "CWE-434",
                                    ));
                                    return Ok((vulnerabilities, tests_run));
                                } else if verify_response.status_code == 200 {
                                    vulnerabilities.push(self.create_vulnerability(
                                        "File Upload Double Extension Bypass",
                                        url,
                                        &format!("Uploaded double extension file '{}' to {}. File accessible but execution not confirmed.", filename, upload_path),
                                        Severity::High,
                                        "CWE-434",
                                    ));
                                    return Ok((vulnerabilities, tests_run));
                                }
                            }
                            Err(_) => continue,
                        }
                    }
                }
                Err(e) => {
                    info!("Double extension test failed: {}", e);
                }
            }
        }

        Ok((vulnerabilities, tests_run))
    }

    /// Create multipart/form-data body
    fn create_multipart_body(
        &self,
        boundary: &str,
        filename: &str,
        content: &str,
        mime_type: &str,
    ) -> String {
        format!(
            "--{}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n{}\r\n--{}--\r\n",
            boundary, filename, mime_type, content, boundary
        )
    }

    /// Create a vulnerability record
    fn create_vulnerability(
        &self,
        vuln_type: &str,
        url: &str,
        evidence: &str,
        severity: Severity,
        cwe: &str,
    ) -> Vulnerability {
        let cvss = match severity {
            Severity::Critical => 9.8,
            Severity::High => 8.1,
            Severity::Medium => 5.3,
            Severity::Low => 3.7,
            Severity::Info => 2.0,
        };

        Vulnerability {
            id: format!("upload_{}", uuid::Uuid::new_v4().to_string()),
            vuln_type: vuln_type.to_string(),
            severity,
            confidence: crate::types::Confidence::Medium,
            category: "File Upload".to_string(),
            url: url.to_string(),
            parameter: None,
            payload: "".to_string(),
            description: format!("{}: {}", vuln_type, evidence),
            evidence: Some(evidence.to_string()),
            cwe: cwe.to_string(),
            cvss: cvss as f32,
            verified: true,
            false_positive: false,
            remediation: self.get_remediation(vuln_type),
            discovered_at: chrono::Utc::now().to_rfc3339(),
                ml_confidence: None,
                ml_data: None,
        }
    }

    /// Get remediation advice based on vulnerability type
    fn get_remediation(&self, vuln_type: &str) -> String {
        match vuln_type {
            "Unrestricted File Upload" => {
                "Implement strict file extension validation using an allow-list (not deny-list). Validate file content and magic bytes, not just the extension. Store uploaded files outside the web root. Use randomized filenames. Implement file size limits. Scan uploads with antivirus.".to_string()
            }
            "File Upload MIME Type Bypass" => {
                "Don't rely solely on MIME type validation. Verify file content and magic bytes. Use an allow-list of permitted file types. Implement server-side validation of file headers. Store files outside web root with no execute permissions.".to_string()
            }
            "File Upload Path Traversal" => {
                "Sanitize filenames to remove path traversal characters (../, .\\, etc.). Use a allow-list of permitted characters. Generate random filenames server-side. Store files in a dedicated directory with no path traversal possible.".to_string()
            }
            "File Upload Double Extension Bypass" => {
                "Validate the complete filename, not just the last extension. Use allow-list validation for extensions. Consider generating filenames server-side. Configure web server to not execute files based on any extension in the filename.".to_string()
            }
            _ => {
                "Implement comprehensive file upload security: use extension allow-lists, validate file content and magic bytes, sanitize filenames, store outside web root, use random filenames, implement size limits, and scan with antivirus.".to_string()
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::ScanConfig;

    fn create_test_scanner() -> FileUploadVulnerabilitiesScanner {
        let client = Arc::new(HttpClient::new(10000, 3).unwrap());
        FileUploadVulnerabilitiesScanner::new(client)
    }

    #[test]
    fn test_extract_upload_paths() {
        let scanner = create_test_scanner();
        let filename = "test.php";
        let base_url = "http://example.com/upload";

        // Test JSON response parsing
        let json_response = r#"{"path":"/uploads/test.php","status":"success"}"#;
        let paths = scanner.extract_upload_paths(json_response, filename, base_url);
        assert!(paths.iter().any(|p| p.contains("/uploads/test.php")));

        // Test common paths are included
        let empty_response = "";
        let paths = scanner.extract_upload_paths(empty_response, filename, base_url);
        assert!(paths.iter().any(|p| p.ends_with("/uploads/test.php")));
        assert!(paths.iter().any(|p| p.ends_with("/files/test.php")));
    }

    #[test]
    fn test_normalize_endpoint() {
        let scanner = create_test_scanner();

        // Test absolute URL
        assert_eq!(
            scanner.normalize_endpoint("http://example.com", "http://other.com/file"),
            "http://other.com/file"
        );

        // Test relative path
        let result = scanner.normalize_endpoint("http://example.com/api", "/uploads/file.txt");
        assert!(result.starts_with("http://"));
        assert!(result.contains("/uploads/file.txt"));
    }

    #[test]
    fn test_create_multipart_body() {
        let scanner = create_test_scanner();

        let body =
            scanner.create_multipart_body("boundary123", "test.txt", "content", "text/plain");

        assert!(body.contains("boundary123"));
        assert!(body.contains("test.txt"));
        assert!(body.contains("text/plain"));
        assert!(body.contains("content"));
    }

    #[test]
    fn test_test_marker_uniqueness() {
        let scanner1 = create_test_scanner();
        let scanner2 = create_test_scanner();

        assert_ne!(scanner1.test_marker, scanner2.test_marker);
        assert!(scanner1.test_marker.starts_with("upload-"));
    }
}