Cat Feeder  1.0.0
The Cat feeder project
Loading...
Searching...
No Matches
sql_injection.py
Go to the documentation of this file.
1r"""
2# +==== BEGIN CatFeeder =================+
3# LOGO:
4# ..............(..../\
5# ...............)..(.')
6# ..............(../..)
7# ...............\‍(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
10# animals/cats
11# /STOP
12# PROJECT: CatFeeder
13# FILE: sql_injection.py
14# CREATION DATE: 11-10-2025
15# LAST Modified: 22:33:21 12-01-2026
16# DESCRIPTION:
17# SQL injection detection module for backend connectors.
18# /STOP
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: Detect, log, and prevent SQL injection attempts.
21# // AR
22# +==== END CatFeeder =================+
23"""
24
25import re
26import base64
27import binascii
28from typing import Union, List, Dict, Any, Callable, Sequence, overload, Optional
29try:
30 from email_validator import validate_email, EmailNotValidError
31except ImportError:
32 EmailNotValidError = None
33 validate_email = None
34
35from display_tty import Disp, initialise_logger
36
37from .sql_constants import RISKY_KEYWORDS, KEYWORD_LOGIC_GATES
38
39
41 """Detect and prevent SQL injection attempts.
42
43 This class provides helper methods to scan strings or nested lists of
44 strings for symbols, keywords, or logical operators commonly used in
45 SQL injection attacks. It also includes utilities for sanitizing input
46 and compiling regex patterns for detection.
47 """
48
49 disp: Disp = initialise_logger(__qualname__, False)
50
51 def __init__(self, error: int = 84, success: int = 0, debug: bool = False) -> None:
52 """Initialize the SQLInjection helper.
53
54 Args:
55 error (int): Numeric error code returned by helper predicates (default is 84).
56 success (int): Numeric success code, unused by predicates (default is 0).
57 debug (bool): Enable debug logging when True (default is False).
58 """
59 # ------------------------ The logging function ------------------------
60 self.disp.update_disp_debug(debug)
61 self.disp.log_debug("Initialising...")
62 # -------------------------- Inherited values --------------------------
63 self.debugdebug: bool = debug
64 # ---------------------------- Status codes ----------------------------
65 self.error: int = error
66 self.success: int = success
67 # --------------------- Injection status variables ---------------------
68 self.injection_err: int = (-1)
69 self.injection_message: str = "Injection attempt detected"
70 # ------------------------ base64 trigger check ------------------------
71 self.base64_key: str = ";base64"
72 # ----------------------- compiled regex indexes -----------------------
73 self.all_key: str = "all"
74 self.symbols_key: str = "symbols"
75 self.keywords_key: str = "keywords"
76 self.logic_gates_key: str = "logic_gates"
77 self.e_mail_key: str = "e-mails"
78 # ------------------ Injection checking related data ------------------
79 _base_symbols: List[str] = [
80 ';', '--', '/*', '*/',
81 '#', '@@', '@', "'", '"', '`', '||'
82 ]
83 _base_keywords: List[str] = [
84 'SELECT', 'INSERT', 'UPDATE', 'DELETE',
85 'DROP', 'CREATE', 'ALTER', 'TABLE', 'UNION', 'JOIN', 'WHERE'
86 ]
87 _base_keywords.extend(RISKY_KEYWORDS)
88 _base_logic_gates: List[str] = ['OR', 'AND', 'NOT']
89 _base_logic_gates.extend(KEYWORD_LOGIC_GATES)
90 # --------------------- Sanitize checking material ---------------------
93 self.command: List[str] = self.keywords
95 _base_logic_gates
96 )
97 # -------------------------- Process regexes --------------------------
98 self.regex_map: Dict[str, Any] = {
99 self.symbols_key: self._compile_patterns(self.symbols),
100 self.keywords_key: self._compile_patterns(self.keywords),
102 # RFC 5322 atom allowed specials set, combined with \w to allow
103 # Unicode letters/digits (RFC 6530). This pattern accepts either
104 # a quoted-string local-part (with escaped chars) or a dot-atom.
105 # It also allows an optional `key=value` prefix and optional
106 # surrounding single/double quotes.
107 self.e_mail_key: re.compile(
108 r"^\s*(?:[^\s=]+\s*=\s*)?['\"]?(?P<local>(?:\"(?:\\.|[^\\\"])+\"|(?:[\w!#$%&'*+/=?^`{|}~-]+(?:\.[\w!#$%&'*+/=?^`{|}~-]+)*)))@(?P<domain>(?:[\w-]+\.)+[\w-]{2,})['\"]?\s*$",
109 re.UNICODE | re.IGNORECASE,
110 )
111 }
112 # ------- Create an instance containing all the sanitised checks -------
113 self.all: List[str] = []
114 self.all.extend(self.symbols)
115 self.all.extend(self.keywords)
116 self.all.extend(self.logic_gates)
117 self.regex_map[self.all_key] = self._compile_patterns(self.all)
118 # ------------------- Negative (safe) test patterns -------------------
120 r"order\s?by\s?(asc|desc)?", # legitimate ORDER BY use
121 r"selective", # word containing 'select'
122 r"unionized", # word containing 'union'
123 ]
124 self.safe_regexes: List[re.Pattern] = []
125 for pat in self.safe_patterns:
126 self.safe_regexes.append(re.compile(pat, re.IGNORECASE))
127 self.disp.log_debug("Initialised")
128
129 # ============================== UTILITIES ============================== #
130
131 @overload
133 self,
134 material_raw: str
135 ) -> str: ...
136
137 @overload
139 self,
140 material_raw: List[str]
141 ) -> List[str]: ...
142
143 def _sanitize_class_checking_material(self, material_raw: Union[List[str], str]) -> Union[List[str], str]:
144 """Sanitize class-level checking material by converting to lowercase.
145
146 Args:
147 material_raw (Union[List[str], str]): Raw material to sanitize.
148
149 Returns:
150 Union[List[str], str]: Sanitized material.
151 """
152 if isinstance(material_raw, list):
153 result = []
154 for i in material_raw:
155 if isinstance(i, str):
156 result.append(i.lower())
157 else:
158 result.append(i)
159 return result
160 if isinstance(material_raw, str):
161 return material_raw.lower()
162 return material_raw
163
164 @overload
166 self,
167 material_raw: str
168 ) -> str: ...
169
170 @overload
172 self,
173 material_raw: List[str]
174 ) -> List[str]: ...
175
176 def _sanitize_usr_input(self, material_raw: Union[List[str], str]) -> Union[List[str], str]:
177 """Sanitize user input by converting to lowercase.
178
179 Args:
180 material_raw (Union[List[str], str]): Raw material to sanitize.
181
182 Returns:
183 Union[List[str], str]: Sanitized material.
184 """
185 if isinstance(material_raw, list):
186 result = []
187 for i in material_raw:
188 if isinstance(i, str):
189 result.append(i.lower())
190 else:
191 result.append(i)
192 return result
193 if isinstance(material_raw, str):
194 return material_raw.lower()
195 return material_raw
196
197 def _compile_patterns(self, tokens: List[str]) -> List[re.Pattern]:
198 """Precompile regex patterns with heuristics for boundary usage.
199
200 Args:
201 tokens (List[str]): Tokens to compile into regex patterns.
202
203 Returns:
204 List[re.Pattern]: Compiled regex patterns.
205 """
206 patterns = []
207 for token in tokens:
208 escaped = re.escape(token)
209 has_special = False
210 for c in token:
211 if c in r".*+?|{}[]()^$\"'\\":
212 has_special = True
213 if token.isalnum() or token.replace("_", "").isalnum():
214 pattern = rf"\b{token}\b"
215 elif has_special:
216 pattern = escaped
217 else:
218 pattern = token
219 patterns.append(re.compile(pattern, re.IGNORECASE))
220 return patterns
221
222 def _is_safe_pattern(self, string: str) -> bool:
223 """Check if string matches known safe patterns.
224
225 Args:
226 string (str): String to check.
227
228 Returns:
229 bool: True if string matches a safe pattern, False otherwise.
230 """
231 for r in self.safe_regexes:
232 if r.search(string):
233 return True
234 return False
235
236 def _is_base64(self, string: str) -> bool:
237 """Check if string is valid base64.
238
239 Args:
240 string (str): Candidate string.
241
242 Returns:
243 bool: True if string decodes as base64, False otherwise.
244 """
245 try:
246 base64.b64decode(string, validate=True)
247 return True
248 except (binascii.Error, ValueError):
249 return False
250
251 # ============================== SCANNERS ============================== #
252
253 def _scan_compiled(self, needle: str, regex_list: List[re.Pattern], parent_function: str) -> bool:
254 """Scan a string against a list of compiled regex patterns.
255
256 Args:
257 needle (str): String to scan.
258 regex_list (List[re.Pattern]): List of compiled regex patterns.
259 parent_function (str): Name of the calling function for logging.
260
261 Returns:
262 bool: True if any pattern matches, False otherwise.
263 True if any pattern matches, False otherwise.
264 """
265 for regex in regex_list:
266 if regex.search(needle):
267 if not self._is_safe_pattern(needle):
268 self.disp.log_debug(
269 f"Failed for {needle}, pattern {regex.pattern} matched.",
270 parent_function
271 )
272 return True
273 return False
274
275 def _is_hex_colour_valid(self, colour: str) -> bool:
276 """Check if a string is a valid CSS-style hex colour code.
277
278 The input is preprocessed:
279 - Strips surrounding whitespace and optional key=value wrappers
280 - Strips surrounding quotes
281 - Rejects internal whitespace
282 - Validates starts with '#' and allowed hex lengths
283
284 Acceptable formats (without '#'):
285 - 3 : RGB
286 - 4 : RGBA
287 - 6 : RRGGBB
288 - 8 : RRGGBBAA
289 - 9 : RRRGGGBBB
290 - 12 : RRRGGGBBBAAA
291
292 Args:
293 colour (str): The colour string to validate.
294
295 Returns:
296 bool: True if valid hex colour, False otherwise.
297 """
298 s = self._extract_wrapped_value(
299 colour, function="_is_hex_colour_valid:_extract_wrapped_value"
300 )
301 if s is None:
302 return False
303
304 # Required preprocessing check — do not alter s
305 if not s.startswith("#"):
306 return False
307
308 # Reject any internal whitespace
309 for c in s:
310 if c.isspace():
311 return False
312
313 hex_part = s[1:]
314 length = len(hex_part)
315
316 # Acceptable hex lengths for CSS-style colors
317 allowed_lengths = {3, 4, 6, 8, 9, 12}
318 if length not in allowed_lengths:
319 return False
320
321 # Validate that all characters are valid hexadecimal digits
322 try:
323 int(hex_part, 16)
324 except ValueError:
325 return False
326
327 return True
328
329 def _is_numeric(self, s: str) -> bool:
330 """Return True if string is purely numeric."""
331 return bool(re.fullmatch(r'\d+(\.\d+)?', s))
332
333 def _extract_email_candidate(self, raw: str, function: str = "_extract_email_candidate") -> Optional[str]:
334 """Extract and return the e-mail candidate from raw string.
335
336 Handles optional ``key=value`` wrappers and surrounding quotes.
337
338 Args:
339 raw (str): Raw string to extract email from.
340
341 Returns:
342 Optional[str]: The inner candidate string or None if it cannot be an e-mail
343 (missing '@' or contains whitespace).
344 """
345 if not isinstance(raw, str) or "@" not in raw:
346 self.disp.log_debug("No @'s in string", function)
347 return None
348
349 s = raw.strip()
350 m_kv = re.match(r"^\s*([^\s=]+)\s*=\s*(.+)$", s)
351 self.disp.log_debug(f"s='{s}', m_kv='{m_kv}'", function)
352 if m_kv:
353 candidate = m_kv.group(2).strip()
354 else:
355 candidate = s
356 self.disp.log_debug(f"candidate='{candidate}'", function)
357
358 if (
359 (candidate.startswith("'") and candidate.endswith("'"))
360 or (candidate.startswith('"') and candidate.endswith('"'))
361 ):
362 self.disp.log_debug(
363 "' or \" found at beginning and end of string, stripping.",
364 function
365 )
366 candidate = candidate[1:-1]
367
368 if re.search(r"\s", candidate):
369 self.disp.log_debug(
370 "string found in candidate, returning None", function)
371 return None
372
373 return candidate
374
375 def _extract_wrapped_value(self, raw: str, function: str = "_extract_wrapped_value") -> Optional[str]:
376 """Extract and normalize a potentially wrapped value.
377
378 Handles optional `key=value` wrappers, strips surrounding quotes and
379 whitespace, and returns None for inputs containing internal whitespace
380 or that are not strings. This mirrors the preprocessing used for
381 e-mail extraction but is generic for other single-value tokens.
382
383 Args:
384 raw (str): Raw input to process.
385 function (str): Caller name for debug logging.
386
387 Returns:
388 Optional[str]: The normalized inner value, or ``None`` if the
389 input is not a valid single token.
390 """
391 if not isinstance(raw, str):
392 self.disp.log_debug("Not a string", function)
393 return None
394
395 s = raw.strip()
396 m_kv = re.match(r"^\s*([^\s=]+)\s*=\s*(.+)$", s)
397 if m_kv:
398 s = m_kv.group(2).strip()
399
400 # Strip surrounding quotes
401 if (s.startswith("'") and s.endswith("'")) or (s.startswith('"') and s.endswith('"')):
402 s = s[1:-1]
403
404 # Reject values containing whitespace
405 if re.search(r"\s", s):
406 self.disp.log_debug(
407 "Whitespace found in candidate, rejecting", function)
408 return None
409
410 return s
411
412 def _is_email(self, raw: str, function: str = "_is_email") -> Optional[str]:
413 """Check if the provided text is an e-mail and return normalized value.
414
415 This accepts either a plain e-mail or a `key=value` pair where the
416 value is an e-mail and returns the normalized e-mail string. If the
417 raw token does not represent an exact e-mail (for example it contains
418 trailing SQL such as ``user@example.com; DROP TABLES;``) this returns
419 None.
420
421 Args:
422 raw (str): The input to check.
423
424 Returns:
425 Optional[str]: Normalized email string if valid, None otherwise.
426 """
427 candidate = self._extract_email_candidate(
428 raw,
429 f"{function}:_extract_email_candidate"
430 )
431 if candidate is None:
432 self.disp.log_debug("Candidate is none, returning none", function)
433 return None
434
435 email_re: re.Pattern = self.regex_map[self.e_mail_key]
436 # require the raw token to fully match the email pattern
437 # this prevents trailing SQL from being accepted
438 if not email_re.fullmatch(raw):
439 self.disp.log_debug("fullmatch failed, returning None", function)
440 return None
441
442 # At this point, our regex has validated the email and confirmed
443 # there's no trailing SQL. We can trust this.
444
445 # prefer the external validator when available; return normalized value
446 if validate_email and EmailNotValidError:
447 self.disp.log_debug(
448 "validate_email and EmailNotValidError are present", function)
449 try:
450 res = validate_email(candidate, check_deliverability=False)
451 self.disp.log_debug(f"res: {res}", function)
452 # Use normalized (newer) or email/ascii_email (deprecated fallback)
453 norm = getattr(res, "normalized", None) or getattr(res, "email", None) or getattr(
454 res, "ascii_email", None) or candidate
455 self.disp.log_debug(f"norm={norm}", function)
456 return norm
457 except EmailNotValidError:
458 # email_validator rejected it, but our regex accepted it
459 # Trust our regex pattern (fullmatch already passed)
460 pass
461
462 # fallback: candidate matched the regex fullmatch and is acceptable
463 self.disp.log_debug(f"candidate={candidate}", function)
464 return candidate
465
466 # ============================== CHECKERS ============================== #
467
468 def check_if_symbol_sql_injection(self, string: Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]]) -> bool:
469 """Detect injection-like symbols in the input.
470
471 This looks for characters or sequences commonly used in SQL
472 injection payloads (for example semicolon or double-dash). If ``string``
473 is a list, each element is checked recursively.
474
475 Args:
476 string (Union[str, None, int, float, Sequence]): String or list of strings to scan.
477
478 Returns:
479 bool: True when an injection-like symbol is detected, False otherwise.
480 """
481 if string is None:
482 return False
483 if isinstance(string, list):
484 for i in string:
486 return True
487 return False
488 raw = str(string)
489 self.disp.log_debug(f"raw: '{raw}'")
490 norm_email = self._is_email(raw)
491 if norm_email:
492 self.disp.log_debug("E-mail found")
493 return False
494 # Treat valid hex colour codes as safe input (e.g. '#FFAABB')
495 if self._is_hex_colour_valid(raw):
496 self.disp.log_debug("Hex colour detected; treating as safe input")
497 return False
499 if self._is_numeric(string):
500 return False
501 if self.base64_key in string:
502 return not self._is_base64(string)
503 final = self._scan_compiled(
504 string,
505 self.regex_map[self.symbols_key],
506 "check_if_symbol_sql_injection:_scan_compiled"
507 )
508 self.disp.log_debug(f"Final response={final}")
509 return final
510
511 def check_if_command_sql_injection(self, string: Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]]) -> bool:
512 """Detect SQL keywords in the input.
513
514 This checks for common SQL command keywords (SELECT, DROP, UNION,
515 etc.). If ``string`` is a list, each element is checked recursively.
516
517 Args:
518 string (Union[str, None, int, float, Sequence]): String or list of strings to scan.
519
520 Returns:
521 bool: True when an SQL keyword is found, False otherwise.
522 """
523 if self.debugdebug:
524 msg = "(check_if_command_sql_injection) string = "
525 msg += f"'{string}', type(string) = '{type(string)}'"
526 self.disp.disp_print_debug(msg)
527 if isinstance(string, list):
528 for i in string:
530 return True
531 return False
532 if string is None:
533 return False
534 raw = str(string)
535 self.disp.log_debug(f"raw: '{raw}'")
536 norm_email = self._is_email(raw)
537 if norm_email:
538 self.disp.log_debug("E-mail found")
539 return False
540 # Treat valid hex colour codes as safe input (e.g. '#FFAABB')
541 if self._is_hex_colour_valid(raw):
542 self.disp.log_debug("Hex colour detected; treating as safe input")
543 return False
545 if self._is_numeric(string):
546 return False
547 if self.base64_key in string:
548 return not self._is_base64(string)
549 final = self._scan_compiled(
550 string,
551 self.regex_map[self.keywords_key],
552 "check_if_command_sql_injection:_scan_compiled"
553 )
554 self.disp.log_debug(f"Final response={final}")
555 return final
556
557 def check_if_logic_gate_sql_injection(self, string: Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]]) -> bool:
558 """Detect logical operators (AND/OR/NOT) in the input.
559
560 Useful to catch attempts that combine conditions to bypass simple
561 filters. Accepts a string or list of strings.
562
563 Args:
564 string (Union[str, None, int, float, Sequence]): String or list of strings to scan.
565
566 Returns:
567 bool: True when a logic gate is present, False otherwise.
568 """
569 if string is None:
570 return False
571 if isinstance(string, list):
572 for i in string:
574 return True
575 return False
576 raw = str(string)
577 self.disp.log_debug(f"raw: '{raw}'")
578 norm_email = self._is_email(raw)
579 if norm_email:
580 self.disp.log_debug("E-mail found")
581 return False
582 # Treat valid hex colour codes as safe input (e.g. '#FFAABB')
583 if self._is_hex_colour_valid(raw):
584 self.disp.log_debug("Hex colour detected; treating as safe input")
585 return False
587 if self._is_numeric(string):
588 return False
589 if self.base64_key in string:
590 return not self._is_base64(string)
591 final = self._scan_compiled(
592 string,
593 self.regex_map[self.logic_gates_key],
594 "check_if_logic_gate_sql_injection:_scan_compiled"
595 )
596 self.disp.log_debug(f"Final response={final}")
597 return final
598
599 def check_if_symbol_and_command_injection(self, string: Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]]) -> bool:
600 """Combined check for symbol- or keyword-based injection patterns.
601
602 Args:
603 string (Union[str, None, int, float, Sequence]): Input to scan.
604
605 Returns:
606 bool: True when either symbol- or keyword-based injection is found.
607 """
608 is_symbol = self.check_if_symbol_sql_injection(string)
609 is_command = self.check_if_command_sql_injection(string)
610 if is_symbol or is_command:
611 return True
612 return False
613
614 def check_if_symbol_and_logic_gate_injection(self, string: Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]]) -> bool:
615 """Combined check for symbol- or logic-gate-based injection patterns.
616
617 Args:
618 string (Union[str, None, int, float, Sequence]): Input to scan.
619
620 Returns:
621 bool: True when symbol- or logic-gate-based injection is found.
622 """
623 is_symbol = self.check_if_symbol_sql_injection(string)
624 is_logic_gate = self.check_if_logic_gate_sql_injection(string)
625 if is_symbol or is_logic_gate:
626 return True
627 return False
628
629 def check_if_command_and_logic_gate_injection(self, string: Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]]) -> bool:
630 """Combined check for keyword- or logic-gate-based injection patterns.
631
632 Args:
633 string (Union[str, None, int, float, Sequence]): Input to scan.
634
635 Returns:
636 bool: True when a command or logic-gate-based injection is found.
637 """
638 is_command = self.check_if_command_sql_injection(string)
639 is_logic_gate = self.check_if_logic_gate_sql_injection(string)
640 if is_command or is_logic_gate:
641 return True
642 return False
643
644 def check_if_sql_injection(self, string: Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]]) -> bool:
645 """High-level SQL injection detection using all configured checks.
646
647 This method runs a combined scan (symbols, keywords, and logic gates)
648 and returns True if any of the component checks considers the input
649 dangerous.
650
651 Args:
652 string (Union[str, None, int, float, Sequence]): Input to scan; may be a string or a
653 list (including nested lists).
654
655 Returns:
656 bool: True when an injection-like pattern is detected, False otherwise.
657 """
658 if string is None:
659 return False
660 if isinstance(string, list):
661 for i in string:
662 if self.check_if_sql_injection(i):
663 return True
664 return False
665 raw = str(string)
666 self.disp.log_debug(f"raw: '{raw}'")
667 norm_email = self._is_email(raw)
668 if norm_email:
669 self.disp.log_debug("E-mail found")
670 return False
671 # Treat valid hex colour codes as safe input (e.g. '#FFAABB')
672 if self._is_hex_colour_valid(raw):
673 self.disp.log_debug("Hex colour detected; treating as safe input")
674 return False
676 if self._is_numeric(string):
677 return False
678 if self.base64_key in string:
679 return not self._is_base64(string)
680 final = self._scan_compiled(
681 string, self.regex_map['all'], "check_if_sql_injection:_scan_compiled")
682 self.disp.log_debug(f"Final response={final}")
683 return final
684
685 def check_if_injections_in_strings(self, array_of_strings: Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]], Sequence[Sequence[Union[str, None, int, float]]]]) -> bool:
686 """Scan an array (possibly nested) of strings for injection patterns.
687
688 This convenience function accepts a string, a list of strings, or a
689 nested list of strings and returns True if any element appears to be
690 an injection.
691
692 Args:
693 array_of_strings (Union[str, None, int, float, Sequence, Sequence[Sequence]]): Item(s) to scan.
694
695 Returns:
696 bool: True when an injection-like value is detected.
697 """
698 if array_of_strings is None:
699 return False
700 if isinstance(array_of_strings, list):
701 for i in array_of_strings:
702 if isinstance(i, list):
703 if self.check_if_injections_in_strings(i) is True:
704 return True
705 continue
706 if self.check_if_sql_injection(i):
707 return True
708 return False
709 strings = self._sanitize_usr_input_sanitize_usr_input_sanitize_usr_input(str(array_of_strings))
710 if self._is_numeric(strings):
711 return False
712 if self.check_if_sql_injection(strings):
713 return True
714 return False
715
716 # ============================== TESTING ============================== #
717
718 def run_test(self, title: str, array: List[Any], function: Callable[[Any], bool], expected_response: bool = False, global_status: int = 0) -> int:
719 """Run a small functional test over the injection-checker functions.
720
721 This helper is used by :meth:`test_injection_class` and not by the
722 production code path. It calls ``function`` for each element in
723 ``array`` and compares the result to ``expected_response``.
724
725 Args:
726 title (str): Short test title printed to stdout.
727 array (List[Any]): Items to test (may be strings or nested lists).
728 function (Callable[[Any], bool]): Function to call for each item.
729 expected_response (bool): Expected boolean response for each call.
730 global_status (int): Running global status to update.
731
732 Returns:
733 int: Updated global status (``0`` for success, error code otherwise).
734 """
735 err = 84
736 global_response = global_status
737 print(f"{title}", end="")
738 for i in array:
739 print(".", end="")
740 if function(i) != expected_response:
741 print("[error]")
742 global_response = err
743 print("[success]")
744 return global_response
745
746 def test_injection_class(self) -> int:
747 """Run a small suite of self-tests for the injection checks.
748
749 Returns:
750 int: ``0`` on success, non-zero error code if any test fails.
751 """
752 success = 0
753 global_status = success
754 test_sentences = [
755 "SHOW TABLES;",
756 "SHOW Databases;",
757 "DROP TABLES;",
758 "SHOW DATABASE;",
759 "SELECT * FROM table;",
760 "ORDER BY ASC"
761 ]
762 global_status = self.run_test(
763 title="Logic gate test:",
764 array=self.logic_gates,
766 expected_response=True,
767 global_status=global_status
768 )
769 global_status = self.run_test(
770 title="Keyword check:",
771 array=self.keywords,
772 function=self.check_if_command_sql_injection,
773 expected_response=True,
774 global_status=global_status
775 )
776 global_status = self.run_test(
777 title="Symbol check:",
778 array=self.symbols,
779 function=self.check_if_symbol_sql_injection,
780 expected_response=True,
781 global_status=global_status
782 )
783 global_status = self.run_test(
784 title="All injections:",
785 array=self.all,
786 function=self.check_if_sql_injection,
787 expected_response=True,
788 global_status=global_status
789 )
790 global_status = self.run_test(
791 title="Array check:",
792 array=[self.all],
793 function=self.check_if_injections_in_strings,
794 expected_response=True,
795 global_status=global_status
796 )
797 global_status = self.run_test(
798 title="Double array check:",
799 array=[self.all, self.all],
800 function=self.check_if_injections_in_strings,
801 expected_response=True,
802 global_status=global_status
803 )
804 global_status = self.run_test(
805 title="SQL sentences:",
806 array=test_sentences,
807 function=self.check_if_sql_injection,
808 expected_response=True,
809 global_status=global_status
810 )
811 return global_status
812
813
814if __name__ == "__main__":
815 DEBUG: bool = True
816 II = SQLInjection(debug=DEBUG)
817 res = II.test_injection_class()
818 print(f"test status = {res}")
None __init__(self, int error=84, int success=0, bool debug=False)
Union[List[str], str] _sanitize_usr_input(self, Union[List[str], str] material_raw)
bool check_if_sql_injection(self, Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]] string)
Union[List[str], str] _sanitize_class_checking_material(self, Union[List[str], str] material_raw)
bool check_if_symbol_and_logic_gate_injection(self, Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]] string)
List[re.Pattern] _compile_patterns(self, List[str] tokens)
int run_test(self, str title, List[Any] array, Callable[[Any], bool] function, bool expected_response=False, int global_status=0)
bool check_if_injections_in_strings(self, Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]], Sequence[Sequence[Union[str, None, int, float]]]] array_of_strings)
str _sanitize_usr_input(self, str material_raw)
bool check_if_command_sql_injection(self, Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]] string)
Optional[str] _is_email(self, str raw, str function="_is_email")
List[str] _sanitize_usr_input(self, List[str] material_raw)
bool check_if_symbol_and_command_injection(self, Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]] string)
bool check_if_logic_gate_sql_injection(self, Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]] string)
bool _scan_compiled(self, str needle, List[re.Pattern] regex_list, str parent_function)
List[str] _sanitize_class_checking_material(self, List[str] material_raw)
str _sanitize_class_checking_material(self, str material_raw)
bool check_if_symbol_sql_injection(self, Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]] string)
bool check_if_command_and_logic_gate_injection(self, Union[Union[str, None, int, float], Sequence[Union[str, None, int, float]]] string)
Optional[str] _extract_email_candidate(self, str raw, str function="_extract_email_candidate")
Optional[str] _extract_wrapped_value(self, str raw, str function="_extract_wrapped_value")