Skip to content

Commit 91bd9ef

Browse files
Copilotfzipi
andauthored
chore: update Python3 bindings to work with upstream libinjection v4.0.0 API (#7)
* Initial plan * Initial plan: update module to work with upstream libinjection Co-authored-by: fzipi <3012076+fzipi@users.noreply.github.com> * Update module to work with upstream libinjection and improve Python 3 compatibility Co-authored-by: fzipi <3012076+fzipi@users.noreply.github.com> * Address review feedback: fix SWIG interface, Makefile, test infrastructure, and add API tests Co-authored-by: fzipi <3012076+fzipi@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: fzipi <3012076+fzipi@users.noreply.github.com>
1 parent 9455021 commit 91bd9ef

10 files changed

Lines changed: 351 additions & 39 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,7 @@ libinjection/libinjection_xss.*
136136
libinjection/libinjection_html5.*
137137
libinjection/libinjection_sqli*
138138
libinjection/libinjection_wrap*
139+
libinjection/libinjection_error.h
140+
141+
# Generated files
142+
words.py

Makefile

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@ all: build
44
#
55

66
build: upstream libinjection/libinjection_wrap.c
7-
rm -f libinjection.py libinjection.pyc
8-
python setup.py --verbose build --force
7+
python3 setup.py --verbose build_ext --inplace
98

109
install: build
11-
sudo python setup.py --verbose install
10+
sudo python3 setup.py --verbose install
1211

1312
test-unit: build words.py
14-
python setup.py build_ext --inplace
15-
PYTHON_PATH='.' nosetests -v --with-xunit test_driver.py
13+
python3 -m pytest test_driver.py -v
1614

1715
.PHONY: test
1816
test: test-unit
@@ -24,16 +22,35 @@ speed:
2422
upstream:
2523
[ -d $@ ] || git clone --depth=1 https://github.com/libinjection/libinjection.git upstream
2624

27-
libinjection/libinjection.h libinjection/libinjection_sqli.h: upstream
25+
libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h \
26+
libinjection/libinjection_xss.h libinjection/libinjection_html5.h: upstream
2827
cp -f upstream/src/libinjection*.h upstream/src/libinjection*.c libinjection/
28+
# Compatibility patches for SWIG wrapping: fix type mismatches and visibility.
29+
# These sed invocations are pattern-matched to avoid breaking unrelated code.
30+
#
31+
# Fix return type mismatch: h5_state_data uses injection_result_t in definition but int in declaration
32+
sed -i 's/^static int h5_state_data(/static injection_result_t h5_state_data(/' libinjection/libinjection_html5.c
33+
# Fix return type mismatch: libinjection_is_sqli declared as injection_result_t but defined as int
34+
sed -i 's/^int libinjection_is_sqli(/injection_result_t libinjection_is_sqli(/' libinjection/libinjection_sqli.c
35+
# Remove static from helper functions so SWIG can wrap and expose them to Python
36+
# (static functions in a header cannot be called from libinjection_wrap.c)
37+
sed -i 's/^static void libinjection_sqli_reset(/void libinjection_sqli_reset(/' libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c
38+
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
39+
sed -i ':a;N;$$!ba;s/static int\nlibinjection_sqli_blacklist/int\nlibinjection_sqli_blacklist/g' libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c
40+
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
2941

3042
words.py: Makefile json2python.py upstream
31-
./json2python.py < upstream/src/sqlparse_data.json > words.py
43+
python3 json2python.py < upstream/src/sqlparse_data.json > words.py
3244

3345

34-
libinjection/libinjection_wrap.c: libinjection/libinjection.i libinjection/libinjection.h libinjection/libinjection_sqli.h
46+
libinjection/libinjection_wrap.c: libinjection/libinjection.i libinjection/libinjection.h \
47+
libinjection/libinjection_sqli.h libinjection/libinjection_error.h \
48+
libinjection/libinjection_xss.h libinjection/libinjection_html5.h
3549
swig -version
36-
swig -py3 -python -builtin -Wall -Wextra libinjection/libinjection.i
50+
swig -python -builtin -Wall -Wextra \
51+
-o libinjection/libinjection_wrap.c \
52+
-outdir libinjection \
53+
libinjection/libinjection.i
3754

3855

3956
.PHONY: copy
@@ -50,5 +67,6 @@ clean:
5067
@rm -f nosetests.xml
5168
@rm -f words.py
5269
@rm -f libinjection/*~ libinjection/*.pyc
53-
@rm -f libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c libinjection/libinjection_sqli_data.h
70+
@rm -f libinjection/libinjection.h libinjection/libinjection_error.h libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c libinjection/libinjection_sqli_data.h
71+
@rm -f libinjection/libinjection_html5.h libinjection/libinjection_html5.c libinjection/libinjection_xss.h libinjection/libinjection_xss.c
5472
@rm -f libinjection/libinjection_wrap.c libinjection/libinjection.py

README.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,97 @@
11
# python3-libinjection
22
libInjection Python3 bindings
3+
4+
## Overview
5+
6+
Python3 bindings for [libinjection](https://github.com/libinjection/libinjection) - a SQL/SQLI tokenizer, parser and analyzer.
7+
8+
## Requirements
9+
10+
- Python 3.x
11+
- SWIG 4.x
12+
- GCC or compatible C compiler
13+
14+
## Building
15+
16+
### 1. Clone the repository and get upstream libinjection
17+
18+
```bash
19+
git clone https://github.com/libinjection/python3-libinjection.git
20+
cd python3-libinjection
21+
make upstream
22+
```
23+
24+
### 2. Copy upstream C source files
25+
26+
```bash
27+
make libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h
28+
```
29+
30+
### 3. Generate the SWIG wrapper
31+
32+
```bash
33+
swig -python -builtin -Wall -Wextra \
34+
-o libinjection/libinjection_wrap.c \
35+
-outdir libinjection \
36+
libinjection/libinjection.i
37+
```
38+
39+
### 4. Build the Python extension
40+
41+
```bash
42+
python3 setup.py build_ext --inplace
43+
```
44+
45+
Or using the Makefile:
46+
47+
```bash
48+
make build
49+
```
50+
51+
### 5. Generate the word lookup table (needed for tests)
52+
53+
```bash
54+
python3 json2python.py < upstream/src/sqlparse_data.json > words.py
55+
```
56+
57+
## Usage
58+
59+
### SQLi Detection
60+
61+
```python
62+
import libinjection
63+
64+
# Simple API - detect SQLi in a string
65+
result, fingerprint = libinjection.sqli("1 UNION SELECT * FROM users")
66+
if result:
67+
print(f"SQLi detected! Fingerprint: {fingerprint}")
68+
69+
# Advanced API with state object
70+
state = libinjection.sqli_state()
71+
libinjection.sqli_init(state, "1 UNION SELECT * FROM users",
72+
libinjection.FLAG_QUOTE_NONE | libinjection.FLAG_SQL_ANSI)
73+
libinjection.sqli_callback(state, None)
74+
if libinjection.is_sqli(state):
75+
print(f"SQLi detected! Fingerprint: {state.fingerprint}")
76+
```
77+
78+
### XSS Detection
79+
80+
```python
81+
import libinjection
82+
83+
# Detect XSS in a string
84+
result = libinjection.xss("<script>alert(1)</script>")
85+
if result:
86+
print("XSS detected!")
87+
```
88+
89+
## Testing
90+
91+
Run the test suite using pytest from the repository root:
92+
93+
```bash
94+
python3 -m pytest test_driver.py test_api.py -v
95+
```
96+
97+
> **Note:** `upstream/tests/` must exist (run `make upstream` first) for `test_driver.py` to find test data.
File renamed without changes.

json2python.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ def toc(obj):
1616
import libinjection
1717
1818
def lookup(state, stype, keyword):
19+
# keyword is passed as bytes from C; decode to str for dict lookup
20+
if isinstance(keyword, bytes):
21+
keyword = keyword.decode('latin-1')
1922
keyword = keyword.upper()
2023
if stype == libinjection.LOOKUP_FINGERPRINT:
2124
if keyword in fingerprints and libinjection.sqli_not_whitelist(state):

libinjection/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from libinjection import *
1+
from .libinjection import *

libinjection/libinjection.i

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
%{
44
#include "libinjection.h"
55
#include "libinjection_sqli.h"
6+
#include "libinjection_xss.h"
7+
#include "libinjection_error.h"
68
#include <stddef.h>
9+
#include <string.h>
710

811
/* This is the callback function that runs a python function
912
*
@@ -13,26 +16,45 @@ static char libinjection_python_check_fingerprint(sfilter* sf, int lookuptype, c
1316
PyObject *fp;
1417
PyObject *arglist;
1518
PyObject *result;
16-
const char* strtype;
1719
char ch;
1820

1921
// get sfilter->pattern
2022
// convert to python string
2123
fp = SWIG_InternalNewPointerObj((void*)sf, SWIGTYPE_p_libinjection_sqli_state,0);
2224

23-
arglist = Py_BuildValue("(Nis#)", fp, lookuptype, word, len);
25+
// Use y# (bytes) format instead of s# (str) to avoid UnicodeDecodeError on
26+
// non-UTF-8 bytes (e.g. 0xA0 word separators). The Python callback will
27+
// receive the word as a bytes object and should decode it as needed.
28+
arglist = Py_BuildValue("(Niy#)", fp, lookuptype, word, len);
29+
if (arglist == NULL) {
30+
// Py_BuildValue failed (e.g., encoding error); treat as not found
31+
return '\0';
32+
}
2433
// call pyfunct with string arg
2534
result = PyObject_CallObject((PyObject*) sf->userdata, arglist);
2635
Py_DECREF(arglist);
2736
if (result == NULL) {
28-
printf("GOT NULL\n");
2937
// python call has an exception
3038
// pass it back
3139
ch = '\0';
3240
} else {
33-
// convert value of python call to a char
34-
strtype = PyString_AsString(result);
35-
ch = strtype[0];
41+
// convert value of python call to a char (Python 3 compatible)
42+
if (PyUnicode_Check(result)) {
43+
Py_ssize_t size;
44+
const char* str = PyUnicode_AsUTF8AndSize(result, &size);
45+
if (str != NULL && size > 0) {
46+
ch = str[0];
47+
} else {
48+
// Clear any exception set by PyUnicode_AsUTF8AndSize on failure
49+
PyErr_Clear();
50+
ch = '\0';
51+
}
52+
} else if (PyBytes_Check(result)) {
53+
const char* str = PyBytes_AsString(result);
54+
ch = (str != NULL) ? str[0] : '\0';
55+
} else {
56+
ch = '\0';
57+
}
3658
Py_DECREF(result);
3759
}
3860
return ch;
@@ -65,8 +87,61 @@ for (i = 0; i < $1_dim0; i++) {
6587
}
6688
}
6789

68-
// automatically append string length into arg array
69-
%apply (char *STRING, size_t LENGTH) { (const char *s, size_t slen) };
90+
// automatically append string length into arg array.
91+
// Accept both str (encoded as UTF-8) and bytes (passed through as-is).
92+
// Using bytes is recommended when the input may contain non-ASCII octets,
93+
// since str will be UTF-8 encoded which changes the byte values.
94+
%typemap(in) (const char *s, size_t slen) (Py_buffer _view, int _must_release) {
95+
_must_release = 0;
96+
if (PyBytes_Check($input)) {
97+
if (PyObject_GetBuffer($input, &_view, PyBUF_SIMPLE) != 0) SWIG_fail;
98+
$1 = (const char *)_view.buf;
99+
$2 = (size_t)_view.len;
100+
_must_release = 1;
101+
} else if (PyUnicode_Check($input)) {
102+
Py_ssize_t _len;
103+
$1 = PyUnicode_AsUTF8AndSize($input, &_len);
104+
if (!$1) SWIG_fail;
105+
$2 = (size_t)_len;
106+
} else {
107+
PyErr_SetString(PyExc_TypeError, "expected str or bytes");
108+
SWIG_fail;
109+
}
110+
}
111+
%typemap(freearg) (const char *s, size_t slen) {
112+
if (_must_release$argnum) PyBuffer_Release(&_view$argnum);
113+
}
114+
%typemap(in) (const char *s, size_t len) (Py_buffer _view, int _must_release) {
115+
_must_release = 0;
116+
if (PyBytes_Check($input)) {
117+
if (PyObject_GetBuffer($input, &_view, PyBUF_SIMPLE) != 0) SWIG_fail;
118+
$1 = (const char *)_view.buf;
119+
$2 = (size_t)_view.len;
120+
_must_release = 1;
121+
} else if (PyUnicode_Check($input)) {
122+
Py_ssize_t _len;
123+
$1 = PyUnicode_AsUTF8AndSize($input, &_len);
124+
if (!$1) SWIG_fail;
125+
$2 = (size_t)_len;
126+
} else {
127+
PyErr_SetString(PyExc_TypeError, "expected str or bytes");
128+
SWIG_fail;
129+
}
130+
}
131+
%typemap(freearg) (const char *s, size_t len) {
132+
if (_must_release$argnum) PyBuffer_Release(&_view$argnum);
133+
}
134+
135+
// Make the fingerprint output parameter in libinjection_sqli() work as an output
136+
// The fingerprint buffer size matches libinjection's internal LIBINJECTION_SQLI_MAX_TOKENS (5) + null byte
137+
#define LIBINJECTION_FINGERPRINT_SIZE 8
138+
%typemap(in, numinputs=0) char fingerprint[] (char temp[LIBINJECTION_FINGERPRINT_SIZE]) {
139+
memset(temp, 0, sizeof(temp));
140+
$1 = temp;
141+
}
142+
%typemap(argout) char fingerprint[] {
143+
$result = SWIG_Python_AppendOutput($result, PyUnicode_FromString($1));
144+
}
70145

71146
%typemap(in) (ptr_lookup_fn fn, void* userdata) {
72147
if ($input == Py_None) {
@@ -77,5 +152,7 @@ for (i = 0; i < $1_dim0; i++) {
77152
$2 = $input;
78153
}
79154
}
155+
%include "libinjection_error.h"
80156
%include "libinjection.h"
81157
%include "libinjection_sqli.h"
158+
%include "libinjection_xss.h"

setup.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,40 @@
55
nickg@client9.com
66
BSD License -- see COPYING.txt for details
77
"""
8+
import os
9+
810
try:
911
from setuptools import setup, Extension
1012
except ImportError:
1113
from distutils.core import setup, Extension
1214

15+
16+
def get_libinjection_version():
17+
"""Read the libinjection version from the upstream source file, if available."""
18+
version_file = os.path.join(os.path.dirname(__file__),
19+
'upstream', 'src', 'libinjection_sqli.c')
20+
if os.path.exists(version_file):
21+
with open(version_file, encoding="utf-8") as f:
22+
for line in f:
23+
if '#define LIBINJECTION_VERSION' in line and '__clang_analyzer__' not in line:
24+
# Extract version string from: #define LIBINJECTION_VERSION "x.y.z"
25+
parts = line.strip().split('"')
26+
if len(parts) >= 2:
27+
return parts[1]
28+
return 'undefined'
29+
30+
31+
LIBINJECTION_VERSION = get_libinjection_version()
32+
1333
MODULE = Extension(
14-
'_libinjection', [
34+
'libinjection._libinjection', [
1535
'libinjection/libinjection_wrap.c',
1636
'libinjection/libinjection_sqli.c',
1737
'libinjection/libinjection_html5.c',
1838
'libinjection/libinjection_xss.c'
1939
],
2040
swig_opts=['-Wextra', '-builtin'],
21-
define_macros = [],
41+
define_macros = [('LIBINJECTION_VERSION', '"{}"'.format(LIBINJECTION_VERSION))],
2242
include_dirs = [],
2343
libraries = [],
2444
library_dirs = [],

0 commit comments

Comments
 (0)