Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,7 @@ libinjection/libinjection_xss.*
libinjection/libinjection_html5.*
libinjection/libinjection_sqli*
libinjection/libinjection_wrap*
libinjection/libinjection_error.h

# Generated files
words.py
38 changes: 28 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ all: build
#

build: upstream libinjection/libinjection_wrap.c
rm -f libinjection.py libinjection.pyc
python setup.py --verbose build --force
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
python3 -m pytest test_driver.py -v

.PHONY: test
test: test-unit
Expand All @@ -24,16 +22,35 @@ 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 \
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
./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 \
libinjection/libinjection_xss.h libinjection/libinjection_html5.h
swig -version
swig -py3 -python -builtin -Wall -Wextra libinjection/libinjection.i
swig -python -builtin -Wall -Wextra \
-o libinjection/libinjection_wrap.c \
-outdir libinjection \
libinjection/libinjection.i


.PHONY: copy
Expand All @@ -50,5 +67,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
95 changes: 95 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,97 @@
# 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 \
-o libinjection/libinjection_wrap.c \
-outdir libinjection \
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("<script>alert(1)</script>")
if result:
print("XSS detected!")
```

## Testing

Run the test suite using pytest from the repository root:

```bash
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.
File renamed without changes.
3 changes: 3 additions & 0 deletions json2python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion libinjection/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from libinjection import *
from .libinjection import *
93 changes: 85 additions & 8 deletions libinjection/libinjection.i
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
%{
#include "libinjection.h"
#include "libinjection_sqli.h"
#include "libinjection_xss.h"
#include "libinjection_error.h"
#include <stddef.h>
#include <string.h>

/* This is the callback function that runs a python function
*
Expand All @@ -13,26 +16,45 @@ 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
// 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);
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);
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';
} else {
ch = '\0';
}
Py_DECREF(result);
}
return ch;
Expand Down Expand Up @@ -65,8 +87,61 @@ 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) };
// 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
#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) {
Expand All @@ -77,5 +152,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"
24 changes: 22 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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"
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 = [],
Expand Down
Loading