├── LICENSE ├── README.md └── lib └── resty └── libinjection.lua /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Robert Paprocki 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Name 2 | ==== 3 | 4 | lua-resty-libinjection - LuaJIT FFI bindings for libinjection 5 | 6 | Status 7 | ====== 8 | 9 | This library is in active development and is production ready 10 | 11 | Description 12 | =========== 13 | 14 | This library provides FFI bindings for [libinjection](https://github.com/client9/libinjection/), a SQL/SQLi and XSS tokenizer and analyzer. 15 | 16 | Installation 17 | ============ 18 | 19 | Install this library as you would any other OpenResty lua module. The libinjection shared object file is required to live in your [lua_package_cpath](https://github.com/openresty/lua-nginx-module#lua_package_cpath), so you should symlink or copy the .so file to the appropriate location. 20 | 21 | Synopsis 22 | ======== 23 | 24 | ```lua 25 | access_by_lua_block { 26 | local libinjection = require "resty.libinjection" 27 | 28 | -- simple SQLi API 29 | local issqli, fingerprint = libinjection.sqli(string) 30 | 31 | -- context-specific bindings are also provided 32 | issqli, fingerprint = libinjection.sqli_noquote(string) 33 | issqli, fingerprint = libinjection.sqli_singlequote(string) 34 | issqli, fingerprint = libinjection.sqli_doublequote(string) 35 | 36 | --simple XSS API 37 | local isxss = libinjection.xss(string) 38 | 39 | -- context-specific bindings 40 | isxss = libinjection.xss_data_state(string) 41 | isxss = libinjection.xss_noquote(string) 42 | isxss = libinjection.xss_singlequote(string) 43 | isxss = libinjection.xss_doublequote(string) 44 | isxss = libinjection.xss_backquote(string) 45 | } 46 | ``` 47 | 48 | Usage 49 | ===== 50 | 51 | libinjection.sqli 52 | ----------------- 53 | 54 | **syntax**: *issqli, fingerprint = libinjection.sqli(string)* 55 | 56 | Given a string, returns a boolean *issqli* representing if SQLi was detected, and a string *fingerprint* representing the libinjection fingerprint detected. In most cases, this is the preferable option; second-level API bindings noted below may be slightly more efficient in specific contexts. 57 | 58 | libinjection.sqli_noquote 59 | ------------------------- 60 | 61 | **syntax**: *issqli, fingerprint = libinjection.sqli_noquote(string)* 62 | 63 | Like [libinjection.sqli](#libinjectionsqli), in the specific context of an "as-is" string. 64 | 65 | libinjection.sqli_singlequote 66 | ----------------------------- 67 | 68 | **syntax**: *issqli, fingerprint = libinjection.sqli_singlequote(string)* 69 | 70 | Like [libinjection.sqli](#libinjectionsqli), in the specific context of a single-quoted (`'`) string. 71 | 72 | libinjection.sqli_doublequote 73 | ----------------------------- 74 | 75 | **syntax**: *issqli, fingerprint = libinjection.sqli_doublelequote(string)* 76 | 77 | Like [libinjection.sqli](#libinjectionsqli), in the specific context of a double-quoted (`"`) string. 78 | 79 | libinjection.xss 80 | ---------------- 81 | 82 | **syntax**: *isxss = libinjection.xss(string)* 83 | 84 | ALPHA version of XSS detector. Given a string, returns a boolean *isxss* denoting if XSS was detected. In most cases, this is the preferable option; second-level API bindings noted below may be slight more efficient in specific contexts. 85 | 86 | libinjection.xss_data_state 87 | --------------------------- 88 | 89 | **syntax**: *isxss = libinjection.xss_data_state(string)* 90 | 91 | Like [libinjection.xss](#libinjectionxss), but run with only the DATA_STATE flag. 92 | 93 | libinjection.xss_noquote 94 | ------------------------ 95 | 96 | **syntax**: *isxss = libinjection.xss_noquote(string)* 97 | 98 | Like [libinjection.xss](#libinjectionxss), but run with only the VALUE_NO_QUOTE flag. 99 | 100 | libinjection.xss_singlequote 101 | ---------------------------- 102 | 103 | **syntax**: *isxss = libinjection.xss_singlequote(string)* 104 | 105 | Like [libinjection.xss](#libinjectionxss), but run with only the VALUE_SINGLE_QUOTE flag. 106 | 107 | libinjection.xss_doublequote 108 | ---------------------------- 109 | 110 | **syntax**: *isxss = libinjection.xss_doublequote(string)* 111 | 112 | Like [libinjection.xss](#libinjectionxss), but run with only the VALUE_DOUBLE_QUOTE flag. 113 | 114 | libinjection.xss_backquote 115 | -------------------------- 116 | 117 | **syntax**: *isxss = libinjection.xss_backquote(string)* 118 | 119 | Like [libinjection.xss](#libinjectionxss), but run with only the VALUE_BACK_QUOTE flag. 120 | 121 | License 122 | ======= 123 | 124 | Copyright 2017 Robert Paprocki 125 | 126 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 127 | 128 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 129 | 130 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 131 | 132 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 133 | 134 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 135 | 136 | 137 | Bugs 138 | ==== 139 | 140 | Please report bugs by creating a ticket with the GitHub issue tracker. 141 | -------------------------------------------------------------------------------- /lib/resty/libinjection.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local bit = require "bit" 4 | local ffi = require "ffi" 5 | 6 | local ffi_new = ffi.new 7 | local ffi_string = ffi.string 8 | 9 | -- enum sqli_flags 10 | local FLAG_NONE = 0 11 | local FLAG_QUOTE_NONE = 1 12 | local FLAG_QUOTE_SINGLE = 2 13 | local FLAG_QUOTE_DOUBLE = 4 14 | local FLAG_SQL_ANSI = 8 15 | local FLAG_SQL_MYSQL = 16 16 | 17 | -- enum lookup_type 18 | local LOOKUP_FINGERPRINT = 4 19 | 20 | -- enum html5_flags 21 | local DATA_STATE = 0 22 | local VALUE_NO_QUOTE = 1 23 | local VALUE_SINGLE_QUOTE = 2 24 | local VALUE_DOUBLE_QUOTE = 3 25 | local VALUE_BACK_QUOTE = 4 26 | 27 | -- cached ORs 28 | local QUOTE_NONE_SQL_ANSI = bit.bor(FLAG_QUOTE_NONE, FLAG_SQL_ANSI) 29 | local QUOTE_NONE_SQL_MYSQL = bit.bor(FLAG_QUOTE_NONE, FLAG_SQL_MYSQL) 30 | local QUOTE_SINGLE_SQL_ANSI = bit.bor(FLAG_QUOTE_SINGLE, FLAG_SQL_ANSI) 31 | local QUOTE_SINGLE_SQL_MYSQL = bit.bor(FLAG_QUOTE_SINGLE, FLAG_SQL_MYSQL) 32 | local QUOTE_DOUBLE_SQL_MYSQL = bit.bor(FLAG_QUOTE_DOUBLE, FLAG_SQL_MYSQL) 33 | 34 | -- libibjection.so 35 | ffi.cdef[[ 36 | const char* libinjection_sqli_fingerprint(struct libinjection_sqli_state* sql_state, int flags); 37 | 38 | struct libinjection_sqli_token { 39 | char type; 40 | char str_open; 41 | char str_close; 42 | size_t pos; 43 | size_t len; 44 | int count; 45 | char val[32]; 46 | }; 47 | 48 | typedef char (*ptr_lookup_fn)(struct libinjection_sqli_state*, int lookuptype, const char* word, size_t len); 49 | 50 | struct libinjection_sqli_state { 51 | const char *s; 52 | size_t slen; 53 | ptr_lookup_fn lookup; 54 | void* userdata; 55 | int flags; 56 | size_t pos; 57 | struct libinjection_sqli_token tokenvec[8]; 58 | struct libinjection_sqli_token *current; 59 | char fingerprint[8]; 60 | int reason; 61 | int stats_comment_ddw; 62 | int stats_comment_ddx; 63 | int stats_comment_c; 64 | int stats_comment_hash; 65 | int stats_folds; 66 | int stats_tokens; 67 | }; 68 | 69 | void libinjection_sqli_init(struct libinjection_sqli_state * sf, const char *s, size_t len, int flags); 70 | int libinjection_is_sqli(struct libinjection_sqli_state* sql_state); 71 | 72 | int libinjection_sqli(const char* s, size_t slen, char fingerprint[]); 73 | 74 | int libinjection_is_xss(const char* s, size_t len, int flags); 75 | int libinjection_xss(const char* s, size_t slen); 76 | ]] 77 | 78 | _M.version = "0.1.1" 79 | 80 | local state_type = ffi.typeof("struct libinjection_sqli_state[1]") 81 | local lib, loaded 82 | 83 | -- "borrowed" from CF aho-corasick lib 84 | local function _loadlib() 85 | if (not loaded) then 86 | local path, so_path 87 | local libname = "libinjection.so" 88 | 89 | for k, v in string.gmatch(package.cpath, "[^;]+") do 90 | so_path = string.match(k, "(.*/)") 91 | if so_path then 92 | -- "so_path" could be nil. e.g, the dir path component is "." 93 | so_path = so_path .. libname 94 | 95 | -- Don't get me wrong, the only way to know if a file exist is 96 | -- trying to open it. 97 | local f = io.open(so_path) 98 | if f ~= nil then 99 | io.close(f) 100 | path = so_path 101 | break 102 | end 103 | end 104 | end 105 | 106 | lib = ffi.load(path) 107 | 108 | if (lib) then 109 | loaded = true 110 | return true 111 | else 112 | return false 113 | end 114 | else 115 | return true 116 | end 117 | end 118 | 119 | -- this function is not publicly exposed so we need to emulate it here. not great but not a measurable perf hit 120 | local function _reparse_as_mysql(sqli_state) 121 | return sqli_state[0].stats_comment_ddx ~= 0 or sqli_state[0].stats_comment_hash ~= 0 122 | end 123 | 124 | --[[ 125 | Secondary API: detects SQLi in a string, given a context. Given a string, returns a list of 126 | 127 | * boolean indicating a match 128 | * SQLi fingerprint 129 | --]] 130 | local function _sqli_contextwrapper(string, char, flag1, flag2) 131 | if (char and not string.find(string, char, 1, true)) then 132 | return false, nil 133 | end 134 | 135 | if (not loaded) then 136 | if (not _loadlib()) then 137 | return false, nil 138 | end 139 | end 140 | 141 | local issqli, lookup, sqli_state 142 | 143 | -- allocate a new libinjection_sqli_state struct 144 | sqli_state = ffi_new(state_type) 145 | 146 | -- init the state 147 | lib.libinjection_sqli_init( 148 | sqli_state, 149 | string, 150 | #string, 151 | FLAG_NONE 152 | ) 153 | 154 | -- initial fingerprint 155 | lib.libinjection_sqli_fingerprint( 156 | sqli_state, 157 | flag1 158 | ) 159 | 160 | -- lookup 161 | lookup = sqli_state[0].lookup( 162 | sqli_state, 163 | LOOKUP_FINGERPRINT, 164 | sqli_state[0].fingerprint, 165 | #ffi.string(sqli_state[0].fingerprint) 166 | ) 167 | 168 | -- match? great, we're done 169 | if (lookup > 0) then 170 | return true, ffi_string(sqli_state[0].fingerprint) 171 | end 172 | 173 | -- no? reparse, fingerprint and lookup again 174 | if (flag2 and _reparse_as_mysql(sqli_state)) then 175 | lib.libinjection_sqli_fingerprint( 176 | sqli_state, 177 | flag2 178 | ) 179 | 180 | lookup = sqli_state[0].lookup( 181 | sqli_state, 182 | LOOKUP_FINGERPRINT, 183 | sqli_state[0].fingerprint, 184 | #ffi.string(sqli_state[0].fingerprint) 185 | ) 186 | 187 | if (lookup > 0) then 188 | return true, ffi_string(sqli_state[0].fingerprint) 189 | end 190 | end 191 | 192 | return false, nil 193 | end 194 | 195 | --[[ 196 | Wrapper for second-level API with no char context 197 | --]] 198 | function _M.sqli_noquote(string) 199 | return _sqli_contextwrapper( 200 | string, 201 | nil, 202 | QUOTE_NONE_SQL_ANSI, 203 | QUOTE_NONE_SQL_MYSQL 204 | ) 205 | end 206 | 207 | --[[ 208 | Wrapper for second-level API with CHAR_SINGLE context 209 | --]] 210 | function _M.sqli_singlequote(string) 211 | return _sqli_contextwrapper( 212 | string, 213 | "'", 214 | QUOTE_SINGLE_SQL_ANSI, 215 | QUOTE_SINGLE_SQL_MYSQL 216 | ) 217 | end 218 | 219 | --[[ 220 | Wrapper for second-level API with CHAR_DOUBLE context 221 | --]] 222 | function _M.sqli_doublequote(string) 223 | return _sqli_contextwrapper( 224 | string, 225 | '"', 226 | QUOTE_DOUBLE_SQL_MYSQL 227 | ) 228 | end 229 | 230 | --[[ 231 | Simple API. Given a string, returns a list of 232 | 233 | * boolean indicating a match 234 | * SQLi fingerprint 235 | --]] 236 | function _M.sqli(string) 237 | if (not loaded) then 238 | if (not _loadlib()) then 239 | return false, nil 240 | end 241 | end 242 | 243 | local fingerprint = ffi_new("char [8]") 244 | 245 | return lib.libinjection_sqli(string, #string, fingerprint) == 1, ffi_string(fingerprint) 246 | end 247 | 248 | --[[ 249 | Secondary API: detects XSS in a string, given a context. Given a string, returns a boolean denoting if XSS was detected 250 | --]] 251 | local function _xss_contextwrapper(string, flag) 252 | if (not loaded) then 253 | if (not _loadlib()) then 254 | return false 255 | end 256 | end 257 | 258 | return lib.libinjection_is_xss(string, #string, flag) == 1 259 | end 260 | 261 | --[[ 262 | Wrapper for second-level API with DATA_STATE flag 263 | --]] 264 | function _M.xss_data_state(string) 265 | return _xss_contextwrapper( 266 | string, 267 | DATA_STATE 268 | ) 269 | end 270 | 271 | --[[ 272 | Wrapper for second-level API with VALUE_NO_QUOTE flag 273 | --]] 274 | function _M.xss_noquote(string) 275 | return _xss_contextwrapper( 276 | string, 277 | VALUE_NO_QUOTE 278 | ) 279 | end 280 | 281 | --[[ 282 | Wrapper for second-level API with VALUE_SINGLE_QUOTE flag 283 | --]] 284 | function _M.xss_singlequote(string) 285 | return _xss_contextwrapper( 286 | string, 287 | VALUE_SINGLE_QUOTE 288 | ) 289 | end 290 | 291 | --[[ 292 | Wrapper for second-level API with VALUE_DOUBLE_QUOTE flag 293 | --]] 294 | function _M.xss_doublequote(string) 295 | return _xss_contextwrapper( 296 | string, 297 | VALUE_DOUBLE_QUOTE 298 | ) 299 | end 300 | 301 | --[[ 302 | Wrapper for second-level API with VALUE_BACK_QUOTE flag 303 | --]] 304 | function _M.xss_backquote(string) 305 | return _xss_contextwrapper( 306 | string, 307 | VALUE_BACK_QUOTE 308 | ) 309 | end 310 | 311 | --[[ 312 | ALPHA version of XSS detector. Given a string, returns a boolean denoting if XSS was detected 313 | --]] 314 | function _M.xss(string) 315 | if (not loaded) then 316 | if (not _loadlib()) then 317 | return false 318 | end 319 | end 320 | 321 | return lib.libinjection_xss(string, #string) == 1 322 | end 323 | 324 | return _M 325 | --------------------------------------------------------------------------------