From 15431156b2d867e35e21b022d71385265a5b84a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:55:45 +0000 Subject: [PATCH 1/4] Initial plan From b9f00295a7d763af2a8c71e93666829015a8450a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:58:38 +0000 Subject: [PATCH 2/4] Initial plan: update module to work with upstream libinjection Co-authored-by: fzipi <3012076+fzipi@users.noreply.github.com> --- libinjection/libinjection_error.h | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 libinjection/libinjection_error.h diff --git a/libinjection/libinjection_error.h b/libinjection/libinjection_error.h new file mode 100644 index 0000000..90c6713 --- /dev/null +++ b/libinjection/libinjection_error.h @@ -0,0 +1,26 @@ +/** + * LibInjection Project + * BSD License -- see `COPYING.txt` for details + * + * https://github.com/libinjection/libinjection/ + * + */ + +#ifndef LIBINJECTION_ERROR_H +#define LIBINJECTION_ERROR_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum injection_result_t { + LIBINJECTION_RESULT_FALSE = 0, + LIBINJECTION_RESULT_TRUE = 1, + LIBINJECTION_RESULT_ERROR = -1 +} injection_result_t; + +#ifdef __cplusplus +} +#endif + +#endif // LIBINJECTION_ERROR_H From e551e7c6159b45b726eda6c5c293d3cefaf299e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:11:07 +0000 Subject: [PATCH 3/4] Update module to work with upstream libinjection and improve Python 3 compatibility Co-authored-by: fzipi <3012076+fzipi@users.noreply.github.com> --- .gitignore | 4 ++ Makefile | 20 +++---- README.md | 91 +++++++++++++++++++++++++++++++ libinjection/__init__.py | 2 +- libinjection/libinjection.i | 32 +++++++++-- libinjection/libinjection_error.h | 26 --------- setup.py | 24 +++++++- 7 files changed, 155 insertions(+), 44 deletions(-) delete mode 100644 libinjection/libinjection_error.h diff --git a/.gitignore b/.gitignore index 3d30f57..c5da1f0 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,7 @@ libinjection/libinjection_xss.* libinjection/libinjection_html5.* libinjection/libinjection_sqli* libinjection/libinjection_wrap* +libinjection/libinjection_error.h + +# Generated files +words.py diff --git a/Makefile b/Makefile index f3aeae4..354c23a 100644 --- a/Makefile +++ b/Makefile @@ -4,15 +4,14 @@ all: build # build: upstream libinjection/libinjection_wrap.c - rm -f libinjection.py libinjection.pyc - python setup.py --verbose build --force + rm -f libinjection/libinjection.py + python3 setup.py --verbose build_ext --inplace install: build - sudo python setup.py --verbose install + sudo python3 setup.py --verbose install test-unit: build words.py - python setup.py build_ext --inplace - PYTHON_PATH='.' nosetests -v --with-xunit test_driver.py + cd /tmp && python3 -m pytest $(CURDIR)/test_driver.py -v .PHONY: test test: test-unit @@ -24,16 +23,16 @@ speed: upstream: [ -d $@ ] || git clone --depth=1 https://github.com/libinjection/libinjection.git upstream -libinjection/libinjection.h libinjection/libinjection_sqli.h: upstream +libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h: upstream cp -f upstream/src/libinjection*.h upstream/src/libinjection*.c libinjection/ words.py: Makefile json2python.py upstream - ./json2python.py < upstream/src/sqlparse_data.json > words.py + python3 json2python.py < upstream/src/sqlparse_data.json > words.py -libinjection/libinjection_wrap.c: libinjection/libinjection.i libinjection/libinjection.h libinjection/libinjection_sqli.h +libinjection/libinjection_wrap.c: libinjection/libinjection.i libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h swig -version - swig -py3 -python -builtin -Wall -Wextra libinjection/libinjection.i + swig -python -builtin -Wall -Wextra libinjection/libinjection.i .PHONY: copy @@ -50,5 +49,6 @@ clean: @rm -f nosetests.xml @rm -f words.py @rm -f libinjection/*~ libinjection/*.pyc - @rm -f libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c libinjection/libinjection_sqli_data.h + @rm -f libinjection/libinjection.h libinjection/libinjection_error.h libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c libinjection/libinjection_sqli_data.h + @rm -f libinjection/libinjection_html5.h libinjection/libinjection_html5.c libinjection/libinjection_xss.h libinjection/libinjection_xss.c @rm -f libinjection/libinjection_wrap.c libinjection/libinjection.py diff --git a/README.md b/README.md index d2937aa..3d85508 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,93 @@ # python3-libinjection libInjection Python3 bindings + +## Overview + +Python3 bindings for [libinjection](https://github.com/libinjection/libinjection) - a SQL/SQLI tokenizer, parser and analyzer. + +## Requirements + +- Python 3.x +- SWIG 4.x +- GCC or compatible C compiler + +## Building + +### 1. Clone the repository and get upstream libinjection + +```bash +git clone https://github.com/libinjection/python3-libinjection.git +cd python3-libinjection +make upstream +``` + +### 2. Copy upstream C source files + +```bash +make libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h +``` + +### 3. Generate the SWIG wrapper + +```bash +swig -python -builtin -Wall -Wextra libinjection/libinjection.i +``` + +### 4. Build the Python extension + +```bash +python3 setup.py build_ext --inplace +``` + +Or using the Makefile: + +```bash +make build +``` + +### 5. Generate the word lookup table (needed for tests) + +```bash +python3 json2python.py < upstream/src/sqlparse_data.json > words.py +``` + +## Usage + +### SQLi Detection + +```python +import libinjection + +# Simple API - detect SQLi in a string +result, fingerprint = libinjection.sqli("1 UNION SELECT * FROM users") +if result: + print(f"SQLi detected! Fingerprint: {fingerprint}") + +# Advanced API with state object +state = libinjection.sqli_state() +libinjection.sqli_init(state, "1 UNION SELECT * FROM users", + libinjection.FLAG_QUOTE_NONE | libinjection.FLAG_SQL_ANSI) +libinjection.sqli_callback(state, None) +if libinjection.is_sqli(state): + print(f"SQLi detected! Fingerprint: {state.fingerprint}") +``` + +### XSS Detection + +```python +import libinjection + +# Detect XSS in a string +result = libinjection.xss("") +if result: + print("XSS detected!") +``` + +## Testing + +Run the test suite using pytest: + +```bash +cd /tmp # run from outside the repo directory to avoid pytest.py conflict +python3 -m pytest /path/to/python3-libinjection/test_driver.py -v +``` diff --git a/libinjection/__init__.py b/libinjection/__init__.py index 84d587a..f16540d 100644 --- a/libinjection/__init__.py +++ b/libinjection/__init__.py @@ -1 +1 @@ -from libinjection import * +from .libinjection import * diff --git a/libinjection/libinjection.i b/libinjection/libinjection.i index 3f279da..8d91dff 100644 --- a/libinjection/libinjection.i +++ b/libinjection/libinjection.i @@ -3,6 +3,8 @@ %{ #include "libinjection.h" #include "libinjection_sqli.h" +#include "libinjection_xss.h" +#include "libinjection_error.h" #include /* This is the callback function that runs a python function @@ -13,7 +15,6 @@ static char libinjection_python_check_fingerprint(sfilter* sf, int lookuptype, c PyObject *fp; PyObject *arglist; PyObject *result; - const char* strtype; char ch; // get sfilter->pattern @@ -25,14 +26,21 @@ static char libinjection_python_check_fingerprint(sfilter* sf, int lookuptype, c result = PyObject_CallObject((PyObject*) sf->userdata, arglist); Py_DECREF(arglist); if (result == NULL) { - printf("GOT NULL\n"); // python call has an exception // pass it back ch = '\0'; } else { - // convert value of python call to a char - strtype = PyString_AsString(result); - ch = strtype[0]; + // convert value of python call to a char (Python 3 compatible) + if (PyUnicode_Check(result)) { + Py_ssize_t size; + const char* str = PyUnicode_AsUTF8AndSize(result, &size); + ch = (str != NULL && size > 0) ? str[0] : '\0'; + } else if (PyBytes_Check(result)) { + const char* str = PyBytes_AsString(result); + ch = (str != NULL) ? str[0] : '\0'; + } else { + ch = '\0'; + } Py_DECREF(result); } return ch; @@ -67,6 +75,18 @@ for (i = 0; i < $1_dim0; i++) { // automatically append string length into arg array %apply (char *STRING, size_t LENGTH) { (const char *s, size_t slen) }; +%apply (char *STRING, size_t LENGTH) { (const char *s, size_t len) }; + +// Make the fingerprint output parameter in libinjection_sqli() work as an output +// The fingerprint buffer size matches libinjection's internal LIBINJECTION_SQLI_MAX_TOKENS (5) + null byte +#define LIBINJECTION_FINGERPRINT_SIZE 8 +%typemap(in, numinputs=0) char fingerprint[] (char temp[LIBINJECTION_FINGERPRINT_SIZE]) { + memset(temp, 0, sizeof(temp)); + $1 = temp; +} +%typemap(argout) char fingerprint[] { + $result = SWIG_Python_AppendOutput($result, PyUnicode_FromString($1)); +} %typemap(in) (ptr_lookup_fn fn, void* userdata) { if ($input == Py_None) { @@ -77,5 +97,7 @@ for (i = 0; i < $1_dim0; i++) { $2 = $input; } } +%include "libinjection_error.h" %include "libinjection.h" %include "libinjection_sqli.h" +%include "libinjection_xss.h" diff --git a/libinjection/libinjection_error.h b/libinjection/libinjection_error.h deleted file mode 100644 index 90c6713..0000000 --- a/libinjection/libinjection_error.h +++ /dev/null @@ -1,26 +0,0 @@ -/** - * LibInjection Project - * BSD License -- see `COPYING.txt` for details - * - * https://github.com/libinjection/libinjection/ - * - */ - -#ifndef LIBINJECTION_ERROR_H -#define LIBINJECTION_ERROR_H - -#ifdef __cplusplus -extern "C" { -#endif - -typedef enum injection_result_t { - LIBINJECTION_RESULT_FALSE = 0, - LIBINJECTION_RESULT_TRUE = 1, - LIBINJECTION_RESULT_ERROR = -1 -} injection_result_t; - -#ifdef __cplusplus -} -#endif - -#endif // LIBINJECTION_ERROR_H diff --git a/setup.py b/setup.py index eb47024..2fdcaad 100644 --- a/setup.py +++ b/setup.py @@ -5,20 +5,40 @@ nickg@client9.com BSD License -- see COPYING.txt for details """ +import os + try: from setuptools import setup, Extension except ImportError: from distutils.core import setup, Extension + +def get_libinjection_version(): + """Read the libinjection version from the upstream source file, if available.""" + version_file = os.path.join(os.path.dirname(__file__), + 'upstream', 'src', 'libinjection_sqli.c') + if os.path.exists(version_file): + with open(version_file) as f: + for line in f: + if '#define LIBINJECTION_VERSION' in line and '__clang_analyzer__' not in line: + # Extract version string from: #define LIBINJECTION_VERSION "x.y.z" + parts = line.strip().split('"') + if len(parts) >= 2: + return parts[1] + return 'undefined' + + +LIBINJECTION_VERSION = get_libinjection_version() + MODULE = Extension( - '_libinjection', [ + 'libinjection._libinjection', [ 'libinjection/libinjection_wrap.c', 'libinjection/libinjection_sqli.c', 'libinjection/libinjection_html5.c', 'libinjection/libinjection_xss.c' ], swig_opts=['-Wextra', '-builtin'], - define_macros = [], + define_macros = [('LIBINJECTION_VERSION', '"{}"'.format(LIBINJECTION_VERSION))], include_dirs = [], libraries = [], library_dirs = [], From beeae1a6842e896d855647dbd4a531b8aaf1ee0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:57:18 +0000 Subject: [PATCH 4/4] Address review feedback: fix SWIG interface, Makefile, test infrastructure, and add API tests Co-authored-by: fzipi <3012076+fzipi@users.noreply.github.com> --- Makefile | 28 +++++++++--- README.md | 12 ++++-- pytest.py => example_sqli.py | 0 json2python.py | 3 ++ libinjection/libinjection.i | 65 +++++++++++++++++++++++++--- setup.py | 2 +- test_api.py | 84 ++++++++++++++++++++++++++++++++++++ test_driver.py | 47 ++++++++++++-------- 8 files changed, 208 insertions(+), 33 deletions(-) rename pytest.py => example_sqli.py (100%) create mode 100644 test_api.py diff --git a/Makefile b/Makefile index 354c23a..ad93292 100644 --- a/Makefile +++ b/Makefile @@ -4,14 +4,13 @@ all: build # build: upstream libinjection/libinjection_wrap.c - rm -f libinjection/libinjection.py python3 setup.py --verbose build_ext --inplace install: build sudo python3 setup.py --verbose install test-unit: build words.py - cd /tmp && python3 -m pytest $(CURDIR)/test_driver.py -v + python3 -m pytest test_driver.py -v .PHONY: test test: test-unit @@ -23,16 +22,35 @@ speed: upstream: [ -d $@ ] || git clone --depth=1 https://github.com/libinjection/libinjection.git upstream -libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h: upstream +libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h \ +libinjection/libinjection_xss.h libinjection/libinjection_html5.h: upstream cp -f upstream/src/libinjection*.h upstream/src/libinjection*.c libinjection/ + # Compatibility patches for SWIG wrapping: fix type mismatches and visibility. + # These sed invocations are pattern-matched to avoid breaking unrelated code. + # + # Fix return type mismatch: h5_state_data uses injection_result_t in definition but int in declaration + sed -i 's/^static int h5_state_data(/static injection_result_t h5_state_data(/' libinjection/libinjection_html5.c + # Fix return type mismatch: libinjection_is_sqli declared as injection_result_t but defined as int + sed -i 's/^int libinjection_is_sqli(/injection_result_t libinjection_is_sqli(/' libinjection/libinjection_sqli.c + # Remove static from helper functions so SWIG can wrap and expose them to Python + # (static functions in a header cannot be called from libinjection_wrap.c) + sed -i 's/^static void libinjection_sqli_reset(/void libinjection_sqli_reset(/' libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c + sed -i ':a;N;$$!ba;s/static char\nlibinjection_sqli_lookup_word/char\nlibinjection_sqli_lookup_word/g' libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c + sed -i ':a;N;$$!ba;s/static int\nlibinjection_sqli_blacklist/int\nlibinjection_sqli_blacklist/g' libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c + sed -i ':a;N;$$!ba;s/static int\nlibinjection_sqli_not_whitelist/int\nlibinjection_sqli_not_whitelist/g' libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c words.py: Makefile json2python.py upstream python3 json2python.py < upstream/src/sqlparse_data.json > words.py -libinjection/libinjection_wrap.c: libinjection/libinjection.i libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h +libinjection/libinjection_wrap.c: libinjection/libinjection.i libinjection/libinjection.h \ +libinjection/libinjection_sqli.h libinjection/libinjection_error.h \ +libinjection/libinjection_xss.h libinjection/libinjection_html5.h swig -version - swig -python -builtin -Wall -Wextra libinjection/libinjection.i + swig -python -builtin -Wall -Wextra \ + -o libinjection/libinjection_wrap.c \ + -outdir libinjection \ + libinjection/libinjection.i .PHONY: copy diff --git a/README.md b/README.md index 3d85508..e22005a 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,10 @@ make libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/l ### 3. Generate the SWIG wrapper ```bash -swig -python -builtin -Wall -Wextra libinjection/libinjection.i +swig -python -builtin -Wall -Wextra \ + -o libinjection/libinjection_wrap.c \ + -outdir libinjection \ + libinjection/libinjection.i ``` ### 4. Build the Python extension @@ -85,9 +88,10 @@ if result: ## Testing -Run the test suite using pytest: +Run the test suite using pytest from the repository root: ```bash -cd /tmp # run from outside the repo directory to avoid pytest.py conflict -python3 -m pytest /path/to/python3-libinjection/test_driver.py -v +python3 -m pytest test_driver.py test_api.py -v ``` + +> **Note:** `upstream/tests/` must exist (run `make upstream` first) for `test_driver.py` to find test data. diff --git a/pytest.py b/example_sqli.py similarity index 100% rename from pytest.py rename to example_sqli.py diff --git a/json2python.py b/json2python.py index 83fedc6..381fc5e 100755 --- a/json2python.py +++ b/json2python.py @@ -16,6 +16,9 @@ def toc(obj): import libinjection def lookup(state, stype, keyword): + # keyword is passed as bytes from C; decode to str for dict lookup + if isinstance(keyword, bytes): + keyword = keyword.decode('latin-1') keyword = keyword.upper() if stype == libinjection.LOOKUP_FINGERPRINT: if keyword in fingerprints and libinjection.sqli_not_whitelist(state): diff --git a/libinjection/libinjection.i b/libinjection/libinjection.i index 8d91dff..2a9b370 100644 --- a/libinjection/libinjection.i +++ b/libinjection/libinjection.i @@ -6,6 +6,7 @@ #include "libinjection_xss.h" #include "libinjection_error.h" #include +#include /* This is the callback function that runs a python function * @@ -21,7 +22,14 @@ static char libinjection_python_check_fingerprint(sfilter* sf, int lookuptype, c // convert to python string fp = SWIG_InternalNewPointerObj((void*)sf, SWIGTYPE_p_libinjection_sqli_state,0); - arglist = Py_BuildValue("(Nis#)", fp, lookuptype, word, len); + // Use y# (bytes) format instead of s# (str) to avoid UnicodeDecodeError on + // non-UTF-8 bytes (e.g. 0xA0 word separators). The Python callback will + // receive the word as a bytes object and should decode it as needed. + arglist = Py_BuildValue("(Niy#)", fp, lookuptype, word, len); + if (arglist == NULL) { + // Py_BuildValue failed (e.g., encoding error); treat as not found + return '\0'; + } // call pyfunct with string arg result = PyObject_CallObject((PyObject*) sf->userdata, arglist); Py_DECREF(arglist); @@ -34,7 +42,13 @@ static char libinjection_python_check_fingerprint(sfilter* sf, int lookuptype, c if (PyUnicode_Check(result)) { Py_ssize_t size; const char* str = PyUnicode_AsUTF8AndSize(result, &size); - ch = (str != NULL && size > 0) ? str[0] : '\0'; + if (str != NULL && size > 0) { + ch = str[0]; + } else { + // Clear any exception set by PyUnicode_AsUTF8AndSize on failure + PyErr_Clear(); + ch = '\0'; + } } else if (PyBytes_Check(result)) { const char* str = PyBytes_AsString(result); ch = (str != NULL) ? str[0] : '\0'; @@ -73,9 +87,50 @@ for (i = 0; i < $1_dim0; i++) { } } -// automatically append string length into arg array -%apply (char *STRING, size_t LENGTH) { (const char *s, size_t slen) }; -%apply (char *STRING, size_t LENGTH) { (const char *s, size_t len) }; +// automatically append string length into arg array. +// Accept both str (encoded as UTF-8) and bytes (passed through as-is). +// Using bytes is recommended when the input may contain non-ASCII octets, +// since str will be UTF-8 encoded which changes the byte values. +%typemap(in) (const char *s, size_t slen) (Py_buffer _view, int _must_release) { + _must_release = 0; + if (PyBytes_Check($input)) { + if (PyObject_GetBuffer($input, &_view, PyBUF_SIMPLE) != 0) SWIG_fail; + $1 = (const char *)_view.buf; + $2 = (size_t)_view.len; + _must_release = 1; + } else if (PyUnicode_Check($input)) { + Py_ssize_t _len; + $1 = PyUnicode_AsUTF8AndSize($input, &_len); + if (!$1) SWIG_fail; + $2 = (size_t)_len; + } else { + PyErr_SetString(PyExc_TypeError, "expected str or bytes"); + SWIG_fail; + } +} +%typemap(freearg) (const char *s, size_t slen) { + if (_must_release$argnum) PyBuffer_Release(&_view$argnum); +} +%typemap(in) (const char *s, size_t len) (Py_buffer _view, int _must_release) { + _must_release = 0; + if (PyBytes_Check($input)) { + if (PyObject_GetBuffer($input, &_view, PyBUF_SIMPLE) != 0) SWIG_fail; + $1 = (const char *)_view.buf; + $2 = (size_t)_view.len; + _must_release = 1; + } else if (PyUnicode_Check($input)) { + Py_ssize_t _len; + $1 = PyUnicode_AsUTF8AndSize($input, &_len); + if (!$1) SWIG_fail; + $2 = (size_t)_len; + } else { + PyErr_SetString(PyExc_TypeError, "expected str or bytes"); + SWIG_fail; + } +} +%typemap(freearg) (const char *s, size_t len) { + if (_must_release$argnum) PyBuffer_Release(&_view$argnum); +} // Make the fingerprint output parameter in libinjection_sqli() work as an output // The fingerprint buffer size matches libinjection's internal LIBINJECTION_SQLI_MAX_TOKENS (5) + null byte diff --git a/setup.py b/setup.py index 2fdcaad..b4010e1 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ def get_libinjection_version(): version_file = os.path.join(os.path.dirname(__file__), 'upstream', 'src', 'libinjection_sqli.c') if os.path.exists(version_file): - with open(version_file) as f: + with open(version_file, encoding="utf-8") as f: for line in f: if '#define LIBINJECTION_VERSION' in line and '__clang_analyzer__' not in line: # Extract version string from: #define LIBINJECTION_VERSION "x.y.z" diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..afaa376 --- /dev/null +++ b/test_api.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +""" +API tests for libinjection Python bindings. +Covers the simple sqli() and xss() APIs as well as the stateful sqli API. +""" +import libinjection + + +def test_sqli_returns_tuple(): + """sqli() should return a (result, fingerprint) sequence.""" + result = libinjection.sqli("1 UNION SELECT * FROM users") + assert len(result) == 2, "sqli() must return a 2-element sequence (result, fingerprint)" + + +def test_sqli_detects_injection(): + """sqli() must detect a known SQLi payload.""" + is_sqli, fingerprint = libinjection.sqli("1 UNION SELECT * FROM users") + assert is_sqli == 1, "Expected SQLi to be detected" + assert fingerprint != "", "Expected non-empty fingerprint for SQLi input" + + +def test_sqli_benign_input(): + """sqli() must not flag benign input.""" + is_sqli, fingerprint = libinjection.sqli("hello world") + assert is_sqli == 0, "Benign input should not be flagged as SQLi" + assert fingerprint == "", "Benign input should produce an empty fingerprint" + + +def test_sqli_fingerprint_content(): + """sqli() fingerprint should be a non-empty string for detected SQLi.""" + is_sqli, fingerprint = libinjection.sqli("1 UNION ALL SELECT * FROM foo") + assert is_sqli == 1 + assert isinstance(fingerprint, str) + assert len(fingerprint) > 0 + + +def test_is_sqli_stateful_api(): + """Advanced stateful API using sqli_state / sqli_init / sqli_callback / is_sqli.""" + state = libinjection.sqli_state() + libinjection.sqli_init( + state, + "1 UNION SELECT * FROM users", + libinjection.FLAG_QUOTE_NONE | libinjection.FLAG_SQL_ANSI, + ) + libinjection.sqli_callback(state, None) + assert libinjection.is_sqli(state) == 1, "Expected SQLi detection via stateful API" + assert state.fingerprint != "", "Expected fingerprint set in state" + + +def test_is_sqli_stateful_benign(): + """Stateful API should not flag benign input.""" + state = libinjection.sqli_state() + libinjection.sqli_init( + state, + "hello world", + libinjection.FLAG_QUOTE_NONE | libinjection.FLAG_SQL_ANSI, + ) + libinjection.sqli_callback(state, None) + assert libinjection.is_sqli(state) == 0, "Benign input should not be SQLi" + + +def test_xss_detects_script_tag(): + """xss() must detect a basic XSS payload.""" + result = libinjection.xss("") + assert result == 1, "Expected XSS detection for