Download runtime/go/sqlctx.go from DibaAi/DibaGuard-Open: direct link, hf CLI and curl.
- Browser
- Download file 24.6 kB
-
https://huggingface.co/DibaAi/DibaGuard-Open/resolve/main/runtime/go/sqlctx.go
- Command line
-
hf download hf://DibaAi/DibaGuard-Open/runtime/go/sqlctx.go
-
curl -L -o sqlctx.go https://huggingface.co/DibaAi/DibaGuard-Open/resolve/main/runtime/go/sqlctx.go
24.6 kB
| package dibaguard | |
| import ( | |
| "regexp" | |
| "sort" | |
| "strings" | |
| "unicode" | |
| "unicode/utf8" | |
| ) | |
| // The hard part of A22 - the SQL-context tier (dibaguard/http/sqlctx.py, judged by dibaguard/shield/database.py | |
| // classify) - inline in the fast path (V-09): every parameter of the canonical text is placed in the query contexts | |
| // applications build (numeric, string, double-quoted, parenthesised, LIKE) and a value that breaks out of one into an | |
| // attack's shape is a hard hit, refused inline like a payload signature. The learned second opinion (DbNet) and the | |
| // soft hits (a request too large to judge) stay with the sidecar, which is sent the same text. | |
| // | |
| // Only the families A22 acts on (injection, rce, privilege, destructive) are ported. Branches anchored at the start of | |
| // the query (^\s*CONFIG SET ..., ^\s*DELETE FROM ...) can never match a query that starts with the context's SELECT and | |
| // are left out; the two back-references (or x = x; "$eq": [n, n]) and the look-aheads are code. Python's \d and \w are | |
| // Unicode-aware and so are the Go classes (\p{Nd}, \p{L}\p{N}_); RE2's \b is ASCII-only, a divergence only for a SQL | |
| // word glued to a non-ASCII letter. Parity: sqlctx_parity in canon_parity.json (TestSQLContextParity). | |
| const ( | |
| sqlWindow = 960 // sqlctx.WINDOW | |
| sqlWindowOverlap = 256 // sqlctx.WINDOW_OVERLAP | |
| sqlBudget = 6144 // sqlctx.BUDGET | |
| ) | |
| // pyRx compiles a Python pattern for RE2: \d and \w become their Unicode classes (Python's str patterns are Unicode). | |
| func pyRx(flags, p string) *regexp.Regexp { | |
| var b strings.Builder | |
| inClass := false | |
| for i := 0; i < len(p); i++ { | |
| c := p[i] | |
| switch { | |
| case c == '\\' && i+1 < len(p): | |
| n := p[i+1] | |
| i++ | |
| switch { | |
| case n == 'd': | |
| b.WriteString(`\p{Nd}`) | |
| case n == 'w' && inClass: | |
| b.WriteString(`\p{L}\p{N}_`) | |
| case n == 'w': | |
| b.WriteString(`[\p{L}\p{N}_]`) | |
| default: | |
| b.WriteByte('\\') | |
| b.WriteByte(n) | |
| } | |
| continue | |
| case c == '[' && !inClass: | |
| inClass = true | |
| b.WriteByte(c) | |
| if i+1 < len(p) && p[i+1] == '^' { | |
| b.WriteByte('^') | |
| i++ | |
| } | |
| if i+1 < len(p) && p[i+1] == ']' { // a ']' first in a class is a literal | |
| b.WriteByte(']') | |
| i++ | |
| } | |
| continue | |
| case c == ']' && inClass: | |
| inClass = false | |
| } | |
| b.WriteByte(c) | |
| } | |
| return regexp.MustCompile(flags + b.String()) | |
| } | |
| var ( | |
| sqlContexts = []struct{ name, tpl string }{ | |
| {"numeric", "SELECT a FROM t WHERE id = {v}"}, | |
| {"string", "SELECT a FROM t WHERE s = '{v}'"}, | |
| {"dquote", "SELECT a FROM t WHERE s = \"{v}\""}, | |
| {"paren_num", "SELECT a FROM t WHERE (id = {v})"}, | |
| {"paren_str", "SELECT a FROM t WHERE (s = '{v}')"}, | |
| {"like", "SELECT a FROM t WHERE s LIKE '%{v}%'"}, | |
| } | |
| sqlSkipHeaders = map[string]bool{"h:accept": true, "h:accept-encoding": true, "h:content-type": true, "h:cache-control": true, | |
| "h:content-length": true, "h:connection": true} | |
| // sqlctx._PRE / _GATE with Python's Unicode word boundaries: a word must not touch a letter, digit or '_' | |
| sqlPre = regexp.MustCompile("(?i)['\"`;()#=<>|&*\\\\]|--|/\\*|(?:^|[^\\p{L}\\p{N}_])(?:select|union|sleep|benchmark|waitfor|or|and|xor|having|order|group|exec|declare|" + | |
| "extractvalue|updatexml|pg_sleep|information_schema|concat|char|chr|cast|convert|drop|insert|delete|update|like|rlike|regexp)(?:$|[^\\p{L}\\p{N}_])") | |
| sqlGate = regexp.MustCompile("(?i)['\"`#]|--|/\\*|(?:^|[^\\p{L}\\p{N}_])(?:(?:select|union|sleep|benchmark|waitfor|pg_sleep|having|exec|execute|declare|information_schema|" + | |
| "extractvalue|updatexml|dual|concat|char|chr|rlike|regexp)(?:$|[^\\p{L}\\p{N}_])|xp_[\\p{L}\\p{N}_]+|(?:or|and|xor)(?:\\s+[\\p{Nd}'\"(@]|['\"(@]))|&&|\\|\\|") | |
| sqlStrongWords = []string{"union", "select", "sleep", "benchmark", "or", "and", "xor", "from", "where", "exec", "waitfor", "extractvalue", "updatexml"} | |
| // database.SIGS / RAW for the families A22 acts on (see the file comment for what is left out and why) | |
| dbInjection = pyRx("(?is)", `(?:\bor|\|\|)\s*\(?\s*(\d+|'s'|0x[0-9a-f]+)\s*(=|like|>|<)\s*(\d+|'s'|0x[0-9a-f]+)`+ | |
| `|\bor\s+(true|not\s+0)\b|\(\s*select\s+(cast|convert)\s*\(`+ | |
| `|(?:\bor|\band|\bxor|\|\||&&)\s*\(?\s*\d+\s*[-*+/]\s*\d+\s*=\s*\d+`+ | |
| `|=\s*\(\s*select\s*[\d(]`+ | |
| `|\b(and|or|xor|&&)\s+\d+\s*[=<>]+\s*\d+\s*(--|#|;|$)`+ | |
| `|\band\s+'s'\s*=\s*'s'\s*;?\s*$`+ | |
| `|(?:\bor|\bxor|\|\|)\s*\(?\s*(-?\d+|'s'|true|false|null)\s+(not\s+)?(between\s+\S+\s+and\s+\S+|in\s*\(|is\s+(not\s+)?(true|false|null|unknown)\b`+ | |
| `|r?like\s|regexp\s|sounds\s+like\s|div\s|mod\s|<=>)`+ | |
| `|(?:\bor|\bxor|\|\|)\s+(-?[1-9]\d*|true|'s')\s*(;|--|#|\)|$)|\bxor\s+(-?\d+|'s'|true)\b`+ | |
| `|(?:\bor|\bxor)\s+(greatest|least|if|ifnull|coalesce|abs|sign|length|ascii|char|concat)\s*\(\s*(-?\d+|'s')\s*(,\s*(-?\d+|'s')\s*)*\)`+ | |
| `|;\s*(and|or|xor)\s`+ | |
| `|\bunion(\s+all|\s+distinct)?\s+select\s+(null\b|\d|0x|concat|char\s*\(|@@|version\s*\(|user\s*\(|database\s*\(|group_concat|'s')`+ | |
| `|;\s*(drop|truncate|delete|insert|update|create|alter|exec|execute|shutdown|grant|copy|declare|waitfor)\b|\b(pg_sleep|sleep|benchmark|randomblob)\s*\(`+ | |
| `|\bwaitfor\s+delay\b|\bdbms_(pipe\.receive_message|lock\.sleep)\b|\b(extractvalue|updatexml)\s*\(|\bcast\s*\(\s*\(\s*select\b`+ | |
| `|\bconvert\s*\(\s*int\s*,\s*\(\s*select\b|\bexp\s*\(\s*~|\bfloor\s*\(\s*rand\s*\(|\b(order|group)\s+by\s+(\d+\s*,\s*){0,9}\d+\s*(--|#)`+ | |
| `|\b(ascii|substring|substr|mid|ord)\s*\(\s*\(\s*select\b|\bif\s*\(\s*\d+\s*=\s*\d+\s*,\s*sleep`+ | |
| `|\b(ascii|substring|substr|mid|ord|length)\s*\(\s*(@@\w+|(database|user|current_user|version|schema)\s*\(\s*\))|\border\s+by\s+\d+\s+order\s+by\b`+ | |
| `|\bunion(\s+all|\s+distinct)?\s+select\b[^;]*\b(from\s+(information_schema|mysql\.|pg_|sys\.|master\.|users|members|accounts|auth_user)`+ | |
| `|password|passwd|authentication_string|pass\b)|\bunion(\s+all|\s+distinct)?\s+select\b[^;]*(--|#)\s*-?\s*$`) | |
| dbRCE = pyRx("(?is)", `\blo_(import|export)\s*\(|\bpg_(read_file|read_binary_file|ls_dir|write_file|stat_file)\s*\(`+ | |
| `|\bxp_(cmdshell|dirtree|fileexist)\b`+ | |
| `|\bsp_oa(create|method)\b|\bopen(rowset|datasource)\s*\(|\bsys_(exec|eval)\s*\(|\bload\s+data\s+(local\s+)?infile\b|\bsoname\b`+ | |
| `|\binto\s+(out|dump)file\b|\bload_file\s*\(|\bdbms_(java|scheduler)\b|\butl_(http|file|tcp|inaddr)\b|\bhttpuritype\b|\$where\b|\$function\b`+ | |
| `|\$accumulator\b|\bmapreduce\b|\brunprogram\s*\(`+ | |
| `|redis\.p?call\s*\(\s*['"](config|slaveof|replicaof|module|debug)['"]`+ | |
| `|\bpackage\.loadlib\b|\bdblink(_exec|_connect)?\s*\(`+ | |
| `|\bjava\.lang\.|\bgetruntime\s*\(|\bapoc\.(load|cypher\.run\w*|import)\w*|\binsert\s+into\s+function\b|\bfrom\s+(url|file|s3|hdfs)\s*\(`+ | |
| `|\bcopy\b.*\b(to|from)\s+program\b|\bcreate\s+(or\s+replace\s+)?function\b.*\blanguage\s+(c|plpythonu|plpython3u|plperlu|plsh|java|javascript)\b`+ | |
| `|\bcreate\s+extension\b.*\b(dblink|plpython\w*|adminpack)\b`) | |
| dbPrivilege = pyRx("(?is)", `\bgrant\s+all\b|\bset\s+global\b|\bsp_addsrvrolemember\b`+ | |
| `|\balter\s+server\s+role\s+sysadmin\b|\bupdate\s+(pg_authid|mysql\.user)\b|\bset\s+role\s+postgres\b|\balter\s+system\s+set\b`+ | |
| `|\bgrant\s+all\s+permissions\b|redis\.p?call\s*\(\s*['"]acl['"]\s*,\s*['"]setuser`+ | |
| `|/_security/(user|role)/\S+\s*\{|\bdbms\.security\.createuser\b|\bgrant\s+role\s+admin\b|\bcreate\s+login\b`+ | |
| `|\balter\s+(user|role)\b.*\b(superuser|createrole|bypassrls|replication)\b|\bgrant\b.*\bto\s+public\b|\bcreate\s+(user|role)\b.*\bsuperuser\b`+ | |
| `|\bgrant\b.*\bwith\s+grant\s+option\b|\b(createuser|grantrolestouser|updateuser)\b.*\b(root|useradminanydatabase|readwriteanydatabase|dbowner|clusteradmin|__system)\b`) | |
| dbDestructive = pyRx("(?is)", `\bdrop\s+(database|schema|table|keyspace|collection)\b|\btruncate\b`+ | |
| `|\bdropdatabase\b|"drop"\s*:\s*"|\.drop\s*\(\s*\)|\b(deletemany|remove)\s*\(\s*\{\s*\}\s*\)|"deletes"\s*:\s*\[\s*\{\s*"q"\s*:\s*\{\s*\}`+ | |
| `|redis\.p?call\s*\(\s*['"](flushall|flushdb|shutdown)['"]`+ | |
| `|\bmatch\s*\(\s*\w+\s*\)\s*(detach\s+)?delete\b`+ | |
| `|"q"\s*:\s*\{\s*\}\s*,\s*"u"\s*:.*"multi"\s*:\s*true|_delete_by_query.*match_all`) | |
| dbRawRCE = pyRx("(?is)", `\bsp_configure\s+'(xp_cmdshell|ole automation procedures)'|\bload\s+csv\s+from\s+'file:|\b(url|file|s3)\s*\(\s*'(https?://(169\.254|metadata)|/)`+ | |
| `|"(script|source|inline|body|map|reduce|init|accumulate)"\s*:\s*"[^"]*(java\.lang|getruntime|runtime\.exec|runprogram|processbuilder)`) | |
| dbRawDestructive = pyRx("(?is)", `\b(btc|bitcoin|monero|xmr)\b.*\b(recover|backed\s+up|pay|send)\b|\b(recover|backed\s+up|pay|send)\b.*\b(btc|bitcoin)\b`+ | |
| `|\bcreate\s+(table|database)\s+\W?\w*(readme|read_me|recover|warning|please_read)`) | |
| dbRawInjection = pyRx("(?is)", `"(pass\w*|pwd|token|secret|api_?key)"\s*:\s*\{\s*"\$(ne|gt|gte|lt|regex|exists|nin|in|not)"|\b(pass\w*|pwd|token)\s*:\s*\{\s*\$(ne|gt|regex|exists)\b`+ | |
| `|"\$or"\s*:\s*\[\s*\{\s*\}`+ | |
| `|"(user(name)?|login|e-?mail)"\s*:\s*\{\s*"\$(ne"\s*:\s*(null|"")\s*\}`+ | |
| `|gte?"\s*:\s*""\s*\}|regex"\s*:\s*"(\^|\.\*))`) | |
| dbExprEq = pyRx("(?is)", `"\$expr"\s*:\s*\{\s*"\$eq"\s*:\s*\[\s*(\d+)\s*,\s*(\d+)\s*\]`) // "$eq": [n, n] (a back-reference in Python) | |
| dbOrSelf = regexp.MustCompile(`(?i)(?:^|[^\p{L}\p{N}_])or|\|\|`) // the start of "or x = x" (a back-reference in Python) | |
| dbSQ = regexp.MustCompile(`'(?:[^'\\]|\\.|'')*'`) | |
| dbSQStd = regexp.MustCompile(`'(?:[^']|'')*'`) // database._SQ_STD: no backslash escapes (standard SQL) | |
| dbVC = pyRx("(?s)", `/\*!\d*\s*(.*?)\*/`) | |
| dbCM = regexp.MustCompile(`(?s)/\*.*?\*/`) | |
| dbLCHash = regexp.MustCompile(`(?:#|--)[^\r\n]*[\r\n]`) | |
| dbTermLit = regexp.MustCompile(`--|#|/\*|;`) | |
| dbNeedRCE = strings.Fields("program lo_ pg_ language xp_ sp_ openrowset opendatasource sys_ infile soname outfile dumpfile load_file dbms_ utl_ httpuritype $where $function $accumulator mapreduce runprogram config slaveof replicaof module system.exec loadlib dblink extension java.lang getruntime apoc function url file s3 hdfs redis. script source inline body map reduce init accumulate processbuilder") | |
| dbNeedPriv = []string{"alter", "grant", "create", "set global", "sp_addsrvrolemember", "sysadmin", "pg_authid", "mysql.user", "set role", | |
| "createuser", "grantrolestouser", "updateuser", "acl", "requirepass", "protected-mode", "_security", "dbms.security"} | |
| dbNeedDestr = []string{"drop", "truncate", "delete", "update", "remove", "deletes", "\"q\"", "flushall", "flushdb", "shutdown", "debug", | |
| "_delete_by_query", "match", "btc", "bitcoin", "monero", "xmr", "recover", "backed", "readme", "read_me", "warning", "please_read", "redis."} | |
| dbNeedInj = []string{" or", "or ", "||", "xor", "and", "&&", "union", ";", "sleep", "benchmark", "randomblob", "waitfor", "dbms_", "extractvalue", | |
| "updatexml", "cast", "convert", "exp", "rand", "order", "group", "ascii", "substr", "mid", "ord", "length", "if", "select", | |
| "$", "pass", "pwd", "token", "secret"} | |
| sqlLeadWords = strings.Fields("or and xor not union select having group order limit procedure into from where like rlike regexp in is between div mod sounds collate case exists glob notnull isnull") | |
| sqlStmtWords = strings.Fields("select insert update delete drop create alter exec execute declare waitfor shutdown grant truncate set show call load with begin handler do") | |
| ) | |
| func isPyWord(r rune) bool { return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' } | |
| func isASCIIWord(c byte) bool { | |
| return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' || c == '_' | |
| } | |
| // hasAny is `any(k in a or k in b for k in need)`. | |
| func hasAny(a, b string, need []string) bool { | |
| for _, k := range need { | |
| if strings.Contains(a, k) || strings.Contains(b, k) { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| // pyFoldWS is database._WS.sub(" ", s).strip(). | |
| func pyFoldWS(s string) string { | |
| var b strings.Builder | |
| b.Grow(len(s)) | |
| space := false | |
| for _, r := range s { | |
| if pyIsSpace(r) { | |
| space = true | |
| continue | |
| } | |
| if space && b.Len() > 0 { | |
| b.WriteByte(' ') | |
| } | |
| space = false | |
| b.WriteRune(r) | |
| } | |
| return b.String() | |
| } | |
| // dbViews is database.views(query, "mysql"): (raw, literals stripped, structural). | |
| func dbViews(q string) (string, string, string) { | |
| raw := q | |
| if runeLen(raw) > 1024 { | |
| rs := []rune(raw) | |
| raw = string(rs[:768]) + " " + string(rs[len(rs)-256:]) | |
| } | |
| s1 := dbSQ.ReplaceAllString(raw, "'s'") | |
| s2 := dbCM.ReplaceAllString(dbVC.ReplaceAllString(s1, " ${1} "), " ") | |
| if strings.ContainsAny(s2, "\n\r") { | |
| s2 = dbLCHash.ReplaceAllString(s2, " ") | |
| } | |
| return raw, s1, pyFoldWS(s2) | |
| } | |
| // orSelf is the back-reference branch `(?:\bor|\|\|)\s+\(?\s*(?P<t1>[\w'()]+)\s*=\s*(?P=t1)(?![\w(])`. | |
| func orSelf(s string) bool { | |
| inT := func(r rune) bool { return isPyWord(r) || r == '\'' || r == '(' || r == ')' } | |
| for _, m := range dbOrSelf.FindAllStringIndex(s, -1) { | |
| p := m[1] // after "or" / "||" (the boundary character before "or" is part of the match) | |
| // \s+ | |
| q := p | |
| for q < len(s) { | |
| r, n := utf8.DecodeRuneInString(s[q:]) | |
| if !pyIsSpace(r) { | |
| break | |
| } | |
| q += n | |
| } | |
| if q == p { | |
| continue | |
| } | |
| starts := []int{q} | |
| if q < len(s) && s[q] == '(' { // \(? consumed, then \s* | |
| k := q + 1 | |
| for k < len(s) { | |
| r, n := utf8.DecodeRuneInString(s[k:]) | |
| if !pyIsSpace(r) { | |
| break | |
| } | |
| k += n | |
| } | |
| starts = append(starts, k) | |
| } | |
| for _, st := range starts { | |
| e := st | |
| for e < len(s) { | |
| r, n := utf8.DecodeRuneInString(s[e:]) | |
| if !inT(r) { | |
| break | |
| } | |
| e += n | |
| } | |
| if e == st { | |
| continue | |
| } | |
| t1 := s[st:e] | |
| k := e | |
| for k < len(s) { | |
| r, n := utf8.DecodeRuneInString(s[k:]) | |
| if !pyIsSpace(r) { | |
| break | |
| } | |
| k += n | |
| } | |
| if k >= len(s) || s[k] != '=' { | |
| continue | |
| } | |
| k++ | |
| for k < len(s) { | |
| r, n := utf8.DecodeRuneInString(s[k:]) | |
| if !pyIsSpace(r) { | |
| break | |
| } | |
| k += n | |
| } | |
| if len(s)-k < len(t1) || !strings.EqualFold(s[k:k+len(t1)], t1) { | |
| continue | |
| } | |
| k += len(t1) | |
| if k < len(s) { | |
| if r, _ := utf8.DecodeRuneInString(s[k:]); isPyWord(r) || r == '(' { | |
| continue | |
| } | |
| } | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| // literalCut is database._literal_cut, the classify fallback for a value placed inside a string literal (V-18): the | |
| // query's first literal is the one the payload closed; up to the first comment or ';' after it there must be nothing | |
| // (admin'--, x';) or SQL (x' or name is not null--). Prose breaks out with letters ("It's great; ..." -> "s great"). | |
| func literalCut(s1 string) bool { | |
| i := strings.IndexByte(s1, '\'') | |
| if i < 0 || !strings.HasPrefix(s1[i:], "'s'") { // the application's literal never closed: no break-out | |
| return false | |
| } | |
| b := i + 3 | |
| lastClose := strings.LastIndex(s1, "*/") | |
| for _, m := range dbTermLit.FindAllStringIndex(s1[b:], -1) { | |
| ts, te := b+m[0], b+m[1] | |
| if s1[ts:te] == "/*" && lastClose >= te { // a closed /* */ cuts nothing: the next one decides | |
| continue | |
| } | |
| return sqlLead(s1[b:ts]) | |
| } | |
| return false | |
| } | |
| // sqlLead is `_NOTHING.fullmatch(lead) or _SQL_LEAD.match(lead)`. | |
| func sqlLead(lead string) bool { | |
| i := 0 | |
| for i < len(lead) { | |
| r, n := utf8.DecodeRuneInString(lead[i:]) | |
| if !pyIsSpace(r) && r != ')' { | |
| break | |
| } | |
| i += n | |
| } | |
| rest := lead[i:] | |
| if rest == "" { | |
| return true | |
| } | |
| word := func(s string, ws []string) bool { | |
| low := strings.ToLower(s) | |
| for _, w := range ws { | |
| if strings.HasPrefix(low, w) && (len(s) == len(w) || !isASCIIWord(s[len(w)])) { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| switch { | |
| case word(rest, sqlLeadWords), strings.HasPrefix(rest, "||"), strings.HasPrefix(rest, "&&"), strings.IndexByte("=<>!+*/%^~|&,?:@", rest[0]) >= 0: | |
| return true | |
| case rest[0] == '-': | |
| return len(rest) == 1 || rest[1] != '-' | |
| case rest[0] == ';': | |
| k := 1 | |
| for k < len(rest) { | |
| r, n := utf8.DecodeRuneInString(rest[k:]) | |
| if !pyIsSpace(r) { | |
| break | |
| } | |
| k += n | |
| } | |
| return word(rest[k:], sqlStmtWords) | |
| } | |
| return false | |
| } | |
| // dbHard is classify(q, "mysql", literal) restricted to the families A22 acts on: true when one of them is found. | |
| func dbHard(q string, literal bool) bool { | |
| raw, s1, s2 := dbViews(q) | |
| ls2, lraw := strings.ToLower(s2), strings.ToLower(raw) | |
| if hasAny(ls2, lraw, dbNeedRCE) && (dbRCE.MatchString(s2) || dbRawRCE.MatchString(raw)) { | |
| return true | |
| } | |
| if hasAny(ls2, lraw, dbNeedPriv) && dbPrivilege.MatchString(s2) { | |
| return true | |
| } | |
| if hasAny(ls2, lraw, dbNeedDestr) && (dbDestructive.MatchString(s2) || dbRawDestructive.MatchString(raw)) { | |
| return true | |
| } | |
| if hasAny(ls2, lraw, dbNeedInj) { | |
| if dbInjection.MatchString(s2) || orSelf(s2) || dbRawInjection.MatchString(raw) { | |
| return true | |
| } | |
| for _, m := range dbExprEq.FindAllStringSubmatch(raw, -1) { | |
| if m[1] == m[2] { | |
| return true | |
| } | |
| } | |
| } | |
| // the break-out as MySQL reads backslashes, and as standard SQL does (x\' closes a PostgreSQL / SQLite literal) | |
| return literal && (literalCut(s1) || strings.Contains(raw, "\\'") && literalCut(dbSQStd.ReplaceAllString(raw, "'s'"))) | |
| } | |
| var swapQuotes = strings.NewReplacer("'", "\"", "\"", "'") | |
| // sqlJudge is sqlctx.judge_value without the learned tier: the first context the value breaks out of, or "". | |
| func sqlJudge(v string) string { | |
| sq := strings.ContainsAny(v, "'\\") | |
| dq := strings.ContainsAny(v, "\"\\") | |
| for _, c := range sqlContexts { | |
| if strings.HasPrefix(c.name, "paren") && !strings.Contains(v, ")") || c.name == "like" && !strings.Contains(v, "%") { | |
| continue | |
| } | |
| if (c.name == "string" || c.name == "paren_str" || c.name == "like") && !sq || c.name == "dquote" && !dq { | |
| continue | |
| } | |
| var q string | |
| if c.name == "dquote" { | |
| q = strings.Replace(sqlContexts[1].tpl, "{v}", swapQuotes.Replace(v), 1) | |
| } else { | |
| q = strings.Replace(c.tpl, "{v}", v, 1) | |
| } | |
| if dbHard(q, c.name == "string" || c.name == "dquote" || c.name == "paren_str" || c.name == "like") { | |
| return c.name | |
| } | |
| } | |
| return "" | |
| } | |
| // unquotePlus is urllib.parse.unquote_plus. | |
| func unquotePlus(s string) string { | |
| s = strings.ReplaceAll(s, "+", " ") | |
| if strings.IndexByte(s, '%') < 0 { | |
| return s | |
| } | |
| b := make([]byte, 0, len(s)) | |
| for i := 0; i < len(s); { | |
| if s[i] == '%' && i+2 < len(s) && ishex(s[i+1]) && ishex(s[i+2]) { | |
| run := make([]byte, 0, 8) | |
| for i+2 < len(s) && s[i] == '%' && ishex(s[i+1]) && ishex(s[i+2]) { | |
| run = append(run, unhex(s[i+1])<<4|unhex(s[i+2])) | |
| i += 3 | |
| } | |
| b = append(b, validUTF8(run)...) | |
| continue | |
| } | |
| b = append(b, s[i]) | |
| i++ | |
| } | |
| return string(b) | |
| } | |
| // pctU is sqlctx._PCTU.sub. | |
| func pctU(s string) string { | |
| if !strings.Contains(s, "%u") { | |
| return s | |
| } | |
| b := make([]byte, 0, len(s)) | |
| for i := 0; i < len(s); i++ { | |
| if s[i] == '%' && i+5 < len(s) && s[i+1] == 'u' && ishex(s[i+2]) && ishex(s[i+3]) && ishex(s[i+4]) && ishex(s[i+5]) { | |
| b = appendRune(b, rune(unhex(s[i+2]))<<12|rune(unhex(s[i+3]))<<8|rune(unhex(s[i+4]))<<4|rune(unhex(s[i+5]))) | |
| i += 5 | |
| continue | |
| } | |
| b = append(b, s[i]) | |
| } | |
| return string(b) | |
| } | |
| // sqlDecode is sqlctx.decode: the value as the application finally sees it. | |
| func sqlDecode(v string) string { | |
| for i := 0; i < 3; i++ { | |
| d := v | |
| if strings.ContainsAny(v, "%+") { | |
| d = unquotePlus(v) | |
| } | |
| d = pctU(d) | |
| if d == v { | |
| break | |
| } | |
| v = d | |
| } | |
| if strings.IndexByte(v, '&') >= 0 { | |
| v = pyUnescapeHTML(v) | |
| } | |
| if strings.IndexByte(v, '\\') >= 0 { // \xHH first, then \uXXXX (the reverse of canon's order) | |
| v = escDecodeOrder(v, []byte{'x', 'u'}) | |
| } | |
| return strings.ReplaceAll(v, "\x00", "") | |
| } | |
| // escDecodeOrder runs one left-to-right pass per escape kind, in the given order. | |
| func escDecodeOrder(s string, order []byte) string { | |
| for _, c := range order { | |
| n := 2 | |
| if c == 'u' { | |
| n = 4 | |
| } | |
| var b []byte | |
| last := 0 | |
| for i := 0; i+1+n < len(s); i++ { | |
| if s[i] != '\\' || s[i+1] != c { | |
| continue | |
| } | |
| ok := true | |
| for k := 0; k < n; k++ { | |
| if !ishex(s[i+2+k]) { | |
| ok = false | |
| break | |
| } | |
| } | |
| if !ok { | |
| continue | |
| } | |
| v := rune(0) | |
| for k := 0; k < n; k++ { | |
| v = v<<4 | rune(unhex(s[i+2+k])) | |
| } | |
| b = append(b, s[last:i]...) | |
| b = appendRune(b, v) | |
| i += 1 + n | |
| last = i + 1 | |
| } | |
| if b != nil { | |
| s = string(append(b, s[last:]...)) | |
| } | |
| } | |
| return s | |
| } | |
| type sqlField struct{ name, v string } | |
| // sqlFields is sqlctx.fields. | |
| func sqlFields(text string) []sqlField { | |
| parts := strings.Split(text, " | ") | |
| var out []sqlField | |
| _, rest, two := strings.Cut(parts[0], " ") | |
| if two && strings.Contains(rest, "?") { | |
| _, q, _ := strings.Cut(rest, "?") | |
| for _, kv := range strings.Split(q, "&") { | |
| k, v, _ := strings.Cut(kv, "=") | |
| out = append(out, sqlField{"q:" + k, sqlDecode(v)}) | |
| } | |
| } | |
| if two { | |
| path, _, _ := strings.Cut(rest, "?") | |
| for _, seg := range strings.Split(path, "/") { | |
| if seg != "" && sqlPre.MatchString(seg) { | |
| out = append(out, sqlField{"path", sqlDecode(seg)}) | |
| } | |
| } | |
| } | |
| for _, p := range parts[1:] { | |
| if runeLen(p) < 3 { | |
| continue | |
| } | |
| _, n0 := utf8.DecodeRuneInString(p) | |
| if r1, _ := utf8.DecodeRuneInString(p[n0:]); r1 != ':' { | |
| continue | |
| } | |
| k, v, _ := strings.Cut(p, "=") | |
| if strings.HasPrefix(p, "h:sec-") || sqlSkipHeaders[k] { | |
| continue | |
| } | |
| if v != "" { | |
| out = append(out, sqlField{k, sqlDecode(v)}) | |
| } | |
| name := sqlDecode(k[n0+1:]) | |
| if sqlPre.MatchString(name) && strings.ContainsAny(name, " '\"(#") { | |
| out = append(out, sqlField{k + "#key", name}, sqlField{k + "#part", sqlDecode(p[n0+1:])}) | |
| } | |
| } | |
| return out | |
| } | |
| // sqlGated is sqlctx.gated: contraction apostrophes (it's, don't, I'm, we'll, they're, I've, I'd) do not count. | |
| func sqlGated(v string) bool { | |
| if strings.IndexByte(v, '\'') >= 0 { | |
| var b strings.Builder | |
| for i := 0; i < len(v); i++ { | |
| if v[i] == '\'' && i > 0 && isLetterASCII(v[i-1]) && contractionAt(v[i+1:]) { | |
| continue | |
| } | |
| b.WriteByte(v[i]) | |
| } | |
| v = b.String() | |
| } | |
| return sqlGate.MatchString(v) | |
| } | |
| func isLetterASCII(c byte) bool { return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' } | |
| func contractionAt(s string) bool { | |
| for _, suf := range []string{"s", "t", "m", "d", "ll", "re", "ve"} { | |
| if strings.HasPrefix(s, suf) && (len(s) == len(suf) || !isASCIIWord(s[len(suf)])) { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| // strongCount is len(sqlctx._STRONG.findall(s)) (case-insensitive, Unicode word boundaries). | |
| func strongCount(s string) int { | |
| n := 0 | |
| prevWord := false | |
| for i := 0; i < len(s); { | |
| c := s[i] | |
| switch { | |
| case c == '\'' || c == '"' || c == '`' || c == '#' || c == ';': | |
| n++ | |
| i++ | |
| prevWord = false | |
| continue | |
| case strings.HasPrefix(s[i:], "--") || strings.HasPrefix(s[i:], "/*"): | |
| n++ | |
| i += 2 | |
| prevWord = false | |
| continue | |
| } | |
| if !prevWord { | |
| low := strings.ToLower(s[i:minInt(len(s), i+16)]) | |
| matched := 0 | |
| for _, w := range sqlStrongWords { | |
| if strings.HasPrefix(low, w) { | |
| if j := i + len(w); j >= len(s) || func() bool { r, _ := utf8.DecodeRuneInString(s[j:]); return !isPyWord(r) }() { | |
| matched = len(w) | |
| break | |
| } | |
| } | |
| } | |
| if matched > 0 { | |
| n++ | |
| i += matched | |
| prevWord = true | |
| continue | |
| } | |
| } | |
| r, w := utf8.DecodeRuneInString(s[i:]) | |
| prevWord = isPyWord(r) | |
| i += w | |
| } | |
| return n | |
| } | |
| func minInt(a, b int) int { | |
| if a < b { | |
| return a | |
| } | |
| return b | |
| } | |
| // sqlWindows is sqlctx.windows (code points). | |
| func sqlWindows(v string) []string { | |
| n := runeLen(v) | |
| if n <= sqlWindow { | |
| return []string{v} | |
| } | |
| rs := []rune(v) | |
| step := sqlWindow - sqlWindowOverlap | |
| var out []string | |
| for i := 0; i < n-sqlWindowOverlap; i += step { | |
| e := i + sqlWindow | |
| if e > n { | |
| e = n | |
| } | |
| out = append(out, string(rs[i:e])) | |
| } | |
| return out | |
| } | |
| // SQLHit is a hard A22 hit: the field, the context it breaks out of, the window judged. | |
| type SQLHit struct { | |
| Field, Context, Value string | |
| } | |
| // SQLContext is the hard part of sqlctx.analyse on a canonical request text: the first field whose value breaks out | |
| // of a query context into an attack's shape, within the same budget and in the same order as the Python tier. | |
| func SQLContext(text string) *SQLHit { | |
| var cand []sqlField | |
| for _, f := range sqlFields(text) { | |
| if runeLen(f.v) >= 2 && sqlGated(f.v) { | |
| cand = append(cand, f) | |
| } | |
| } | |
| if len(cand) > 1 { | |
| key := make([]int, len(cand)) | |
| for i, f := range cand { | |
| key[i] = strongCount(runeCut(f.v, sqlWindow)) | |
| } | |
| idx := make([]int, len(cand)) | |
| for i := range idx { | |
| idx[i] = i | |
| } | |
| sort.SliceStable(idx, func(a, b int) bool { return key[idx[a]] > key[idx[b]] }) | |
| c2 := make([]sqlField, len(cand)) | |
| for i, j := range idx { | |
| c2[i] = cand[j] | |
| } | |
| cand = c2 | |
| } | |
| budget := sqlBudget | |
| for _, f := range cand { | |
| for _, w := range sqlWindows(f.v) { | |
| wl := runeLen(w) | |
| if wl < 2 || !sqlGated(w) { | |
| continue | |
| } | |
| if wl > budget { | |
| return nil // the budget ran out: a soft hit, the sidecar's to answer | |
| } | |
| budget -= wl | |
| if ctx := sqlJudge(w); ctx != "" { | |
| return &SQLHit{Field: f.name, Context: ctx, Value: runeCut(w, 200)} | |
| } | |
| } | |
| } | |
| return nil | |
| } | |