Merge pull request #129 from samwafgo/fix_xss

feat:update libinjection version
This commit is contained in:
samwafgo
2025-02-05 10:37:45 +08:00
committed by GitHub
495 changed files with 13 additions and 16969 deletions
+13
View File
@@ -0,0 +1,13 @@
package libinjection
import (
"github.com/corazawaf/libinjection-go"
)
func IsXSS(input string) bool {
return libinjection.IsXSS(input)
}
func IsSQLiNotReturnPrint(input string) bool {
result, _ := libinjection.IsSQLi(input)
return result
}
-607
View File
@@ -1,607 +0,0 @@
package libinjection
import (
"strings"
)
func (h *h5State) skipWhite() int {
for h.pos < h.len {
ch := h.s[h.pos]
switch ch {
case 0x00, 0x20, 0x09, 0x0A, 0x0B, 0x0C, 0x0D:
h.pos++
default:
return int(ch)
}
}
return byteEOF
}
func (h *h5State) stateEOF() bool {
return false
}
// 12.2.4.44
func (h *h5State) stateBogusComment() bool {
index := strings.IndexByte(h.s[h.pos:], byteGT)
if index == -1 {
h.tokenStart = h.s[h.pos:]
h.tokenLen = h.len - h.pos
h.pos = h.len
h.state = h.stateEOF
} else {
h.tokenStart = h.s[h.pos:]
h.tokenLen = index
h.pos = h.pos + index + 1
h.state = h.stateData
}
h.tokenType = html5TypeTagComment
return true
}
// 12.2.4.44 ALT
func (h *h5State) stateBogusComment2() bool {
pos := h.pos
for {
index := strings.IndexByte(h.s[pos:], bytePercent)
if index == -1 || pos+index+1 >= h.len {
h.tokenStart = h.s[h.pos:]
h.tokenLen = h.len - h.pos
h.pos = h.len
h.tokenType = html5TypeTagComment
h.state = h.stateEOF
return true
}
if h.s[h.pos+index+1] != byteGT {
pos = pos + index + 1
continue
}
// ends in %>
h.tokenStart = h.s[h.pos:]
h.tokenLen = index
h.pos = pos + index + 2
h.state = h.stateData
h.tokenType = html5TypeTagComment
return true
}
}
// 12.2.4.48
// 12.2.4.49
// 12.2.4.50
// 12.2.4.51
// state machine spec is confusing since it can only look
// at one character at a time but simply it's comments end by:
// 1) EOF
// 2) ending in -->
// 3) ending in -!>
func (h *h5State) stateComment() bool {
pos := h.pos
for {
index := strings.IndexByte(h.s[pos:], byteDash)
// did not find anything or has less than 3 characters
if index == -1 || pos+index+3 > h.len {
h.state = h.stateEOF
h.tokenStart = h.s[h.pos:]
h.tokenLen = h.len - h.pos
h.tokenType = html5TypeTagComment
return true
}
offset := 1
// skip all nulls
for pos+index+offset < h.len && h.s[pos+index+offset] == 0x00 {
offset++
}
if pos+index+offset == h.len {
h.state = h.stateEOF
h.tokenStart = h.s[h.pos:]
h.tokenLen = h.len - h.pos
h.tokenType = html5TypeTagComment
return true
}
ch := h.s[pos+index+offset]
if ch != byteDash && ch != byteBang {
pos = pos + index + 1
continue
}
offset++
if pos+index+offset == h.len {
h.state = h.stateEOF
h.tokenStart = h.s[h.pos:]
h.tokenLen = h.len - h.pos
h.tokenType = html5TypeTagComment
return true
}
if h.s[pos+index+offset] != byteGT {
pos = pos + index + 1
continue
}
offset++
// ends in --> or -!>
h.tokenStart = h.s[h.pos:]
h.tokenLen = index + pos - h.pos
h.pos = pos + index + offset
h.state = h.stateData
h.tokenType = html5TypeTagComment
return true
}
}
func (h *h5State) stateCData() bool {
pos := h.pos
for {
index := strings.IndexByte(h.s[pos:], byteRightB)
// did not find anything or has less 3 chars left
switch {
case index == -1 || h.pos+index+3 > h.len:
h.state = h.stateEOF
h.tokenStart = h.s[h.pos:]
h.tokenLen = h.len - h.pos
h.tokenType = html5TypeDataText
return true
case h.s[pos+index+1] == byteRightB && h.s[pos+index+2] == byteGT:
h.state = h.stateData
h.tokenStart = h.s[h.pos:]
h.tokenLen = pos + index - h.pos
h.pos = pos + index + 3
h.tokenType = html5TypeDataText
return true
default:
pos = pos + index + 1
}
}
}
func (h *h5State) stateDoctype() bool {
h.tokenStart = h.s[h.pos:]
h.tokenType = html5TypeDocType
index := strings.IndexByte(h.s[h.pos:], byteGT)
if index == -1 {
h.state = h.stateEOF
h.tokenLen = h.len - h.pos
} else {
h.state = h.stateData
h.tokenLen = index
h.pos = h.pos + index + 1
}
return true
}
func (h *h5State) stateMarkupDeclarationOpen() bool {
remaining := h.len - h.pos
switch {
case remaining >= 7 &&
strings.ToLower(h.s[h.pos:h.pos+7]) == "doctype":
return h.stateDoctype()
case remaining >= 7 &&
h.s[h.pos:h.pos+7] == "[CDATA[":
h.pos += 7
return h.stateCData()
case remaining >= 2 &&
h.s[h.pos:h.pos+2] == "--":
h.pos += 2
return h.stateComment()
}
return h.stateBogusComment()
}
func (h *h5State) stateSelfClosingStartTag() bool {
if h.pos >= h.len {
return false
}
ch := h.s[h.pos]
if ch == byteGT {
h.tokenStart = h.s[h.pos-1:]
h.tokenLen = 2
h.tokenType = html5TypeTagNameSelfClose
h.state = h.stateData
h.pos++
return true
}
return h.stateBeforeAttributeName()
}
func (h *h5State) stateTagNameClose() bool {
h.isClose = false
h.tokenStart = h.s[h.pos:]
h.tokenLen = 1
h.tokenType = html5TypeTagNameClose
h.pos++
if h.pos < h.len {
h.state = h.stateData
} else {
h.state = h.stateEOF
}
return true
}
// 12.2.4.10
func (h *h5State) stateTagName() bool {
pos := h.pos
for pos < h.len {
ch := h.s[pos]
switch {
case ch == 0:
// special non-standard case
// allow nulls in tag name
// some old browsers apparently allow and ignore them
pos++
case isH5White(ch):
h.tokenStart = h.s[h.pos:]
h.tokenLen = pos - h.pos
h.tokenType = html5TypeTagNameOpen
h.pos = pos + 1
h.state = h.stateBeforeAttributeName
return true
case ch == byteSlash:
h.tokenStart = h.s[h.pos:]
h.tokenLen = pos - h.pos
h.tokenType = html5TypeTagNameOpen
h.pos = pos + 1
h.state = h.stateSelfClosingStartTag
return true
case ch == byteGT:
h.tokenStart = h.s[h.pos:]
h.tokenLen = pos - h.pos
if h.isClose {
h.pos = pos + 1
h.isClose = false
h.tokenType = html5TypeTagClose
h.state = h.stateData
} else {
h.pos = pos
h.tokenType = html5TypeTagNameOpen
h.state = h.stateTagNameClose
}
return true
default:
pos++
}
}
h.tokenStart = h.s[h.pos:]
h.tokenLen = h.len - h.pos
h.tokenType = html5TypeTagNameOpen
h.state = h.stateEOF
return true
}
// 12.2.4.9
func (h *h5State) stateEndTagOpen() bool {
if h.pos >= h.len {
return false
}
ch := h.s[h.pos]
if ch == byteGT {
return h.stateData()
} else if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') {
return h.stateTagName()
}
h.isClose = false
return h.stateBogusComment()
}
func (h *h5State) stateTagOpen() bool {
if h.pos >= h.len {
return false
}
ch := h.s[h.pos]
switch {
case ch == byteBang:
h.pos++
return h.stateMarkupDeclarationOpen()
case ch == byteSlash:
h.pos++
h.isClose = true
return h.stateEndTagOpen()
case ch == byteQuestion:
h.pos++
return h.stateBogusComment()
case ch == bytePercent:
// this is not in spec.. alternative comment format used
// by IE <= 9 and Safari < 4.0.3
h.pos++
return h.stateBogusComment2()
case (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'):
return h.stateTagName()
case ch == byteNull:
// IE-ism NULL characters are ignored
return h.stateTagName()
default:
// user input mistake in configuring state
if h.pos == 0 {
return h.stateData()
}
h.tokenStart = h.s[h.pos-1:]
h.tokenLen = 1
h.tokenType = html5TypeDataText
h.state = h.stateData
return true
}
}
func (h *h5State) stateData() bool {
index := strings.IndexByte(h.s[h.pos:], byteLT)
if index == -1 {
h.tokenStart = h.s[h.pos:]
h.tokenLen = h.len - h.pos
h.tokenType = html5TypeDataText
h.state = h.stateEOF
if h.tokenLen == 0 {
return false
}
} else {
h.tokenStart = h.s[h.pos:]
h.tokenType = html5TypeDataText
h.tokenLen = index
h.pos = h.pos + index + 1
h.state = h.stateTagOpen
if h.tokenLen == 0 {
return h.stateTagOpen()
}
}
return true
}
func (h *h5State) stateAttributeValueNoQuote() bool {
pos := h.pos
for pos < h.len {
ch := h.s[pos]
if isH5White(ch) {
h.tokenType = html5TypeAttrValue
h.tokenStart = h.s[h.pos:]
h.tokenLen = pos - h.pos
h.pos = pos + 1
h.state = h.stateBeforeAttributeName
return true
} else if ch == byteGT {
h.tokenType = html5TypeAttrValue
h.tokenStart = h.s[h.pos:]
h.tokenLen = pos - h.pos
h.pos = pos
h.state = h.stateTagNameClose
return true
}
pos++
}
h.state = h.stateEOF
h.tokenStart = h.s[h.pos:]
h.tokenLen = h.len - h.pos
h.tokenType = html5TypeAttrValue
return true
}
// 12.2.4.37
func (h *h5State) stateBeforeAttributeValue() bool {
ch := h.skipWhite()
if ch == byteEOF {
h.state = h.stateEOF
return false
}
switch uint8(ch) {
case byteDouble:
return h.stateAttributeValueDoubleQuote()
case byteSingle:
return h.stateAttributeValueSingleQuote()
case byteTick:
// non standard IE
return h.stateAttributeValueBackQuote()
default:
return h.stateAttributeValueNoQuote()
}
}
func (h *h5State) stateAfterAttributeName() bool {
ch := h.skipWhite()
switch ch {
case byteEOF:
return false
case byteSlash:
h.pos++
return h.stateSelfClosingStartTag()
case byteEquals:
h.pos++
return h.stateBeforeAttributeValue()
case byteGT:
return h.stateTagNameClose()
default:
return h.stateAttributeName()
}
}
func (h *h5State) stateAttributeName() bool {
pos := h.pos + 1
for pos < h.len {
ch := h.s[pos]
switch {
case isH5White(ch):
h.tokenStart = h.s[h.pos:]
h.tokenLen = pos - h.pos
h.tokenType = html5TypeAttrName
h.state = h.stateAfterAttributeName
h.pos = pos + 1
return true
case ch == byteSlash:
h.tokenStart = h.s[h.pos:]
h.tokenLen = pos - h.pos
h.tokenType = html5TypeAttrName
h.state = h.stateSelfClosingStartTag
h.pos = pos + 1
return true
case ch == byteEquals:
h.tokenStart = h.s[h.pos:]
h.tokenLen = pos - h.pos
h.tokenType = html5TypeAttrName
h.state = h.stateBeforeAttributeValue
h.pos = pos + 1
return true
case ch == byteGT:
h.tokenStart = h.s[h.pos:]
h.tokenLen = pos - h.pos
h.tokenType = html5TypeAttrName
h.state = h.stateTagNameClose
h.pos = pos
return true
default:
pos++
}
}
// EOF
h.tokenStart = h.s[h.pos:]
h.tokenLen = h.len - h.pos
h.tokenType = html5TypeAttrName
h.state = h.stateEOF
h.pos = h.len
return true
}
func (h *h5State) stateBeforeAttributeName() bool {
ch := h.skipWhite()
switch ch {
case byteEOF:
return false
case byteSlash:
h.pos++
return h.stateSelfClosingStartTag()
case byteGT:
h.state = h.stateData
h.tokenStart = h.s[h.pos:]
h.tokenLen = 1
h.tokenType = html5TypeTagNameClose
h.pos++
return true
default:
return h.stateAttributeName()
}
}
// 12.2.4.41
func (h *h5State) stateAfterAttributeValueQuotedState() bool {
if h.pos >= h.len {
return false
}
ch := h.s[h.pos]
switch {
case isH5White(ch):
h.pos++
return h.stateBeforeAttributeName()
case ch == byteSlash:
h.pos++
return h.stateSelfClosingStartTag()
case ch == byteGT:
h.tokenStart = h.s[h.pos:]
h.tokenLen = 1
h.tokenType = html5TypeTagNameClose
h.pos++
h.state = h.stateData
return true
default:
return h.stateBeforeAttributeName()
}
}
func (h *h5State) stateAttributeValueQuote(ch byte) bool {
// skip initial quote in normal case.
// don't do this "if (pos == 0)" since it means we have started
// in a non-data state. given an input of '><foo
// we want to make 0-length attribute name
if h.pos > 0 {
h.pos++
}
index := strings.IndexByte(h.s[h.pos:], ch)
if index == -1 {
h.tokenStart = h.s[h.pos:]
h.tokenLen = h.len - h.pos
h.tokenType = html5TypeAttrValue
h.state = h.stateEOF
} else {
h.tokenStart = h.s[h.pos:]
h.tokenLen = index
h.tokenType = html5TypeAttrValue
h.state = h.stateAfterAttributeValueQuotedState
h.pos += h.tokenLen + 1
}
return true
}
func (h *h5State) stateAttributeValueSingleQuote() bool {
return h.stateAttributeValueQuote(byteSingle)
}
func (h *h5State) stateAttributeValueDoubleQuote() bool {
return h.stateAttributeValueQuote(byteDouble)
}
func (h *h5State) stateAttributeValueBackQuote() bool {
return h.stateAttributeValueQuote(byteTick)
}
func (h *h5State) init(input string, flags int) {
h.s = input
h.len = len(input)
switch flags {
case html5FlagsDataState:
h.state = h.stateData
case html5FlagsValueNoQuote:
h.state = h.stateBeforeAttributeName
case html5FlagsValueSingleQuote:
h.state = h.stateAttributeValueSingleQuote
case html5FlagsValueDoubleQuote:
h.state = h.stateAttributeValueDoubleQuote
case html5FlagsValueBackQuote:
h.state = h.stateAttributeValueBackQuote
}
}
func (h *h5State) next() bool {
return h.state()
}
-48
View File
@@ -1,48 +0,0 @@
package libinjection
const (
byteEOF = -1
byteBang = 33
bytePercent = 37
byteDash = 45
byteSlash = 47
byteLT = 60
byteEquals = 61
byteGT = 62
byteQuestion = 63
byteRightB = 93
)
const (
html5TypeDataText = iota
html5TypeTagNameOpen
html5TypeTagNameClose
html5TypeTagNameSelfClose
html5TypeTagData
html5TypeTagClose
html5TypeAttrName
html5TypeAttrValue
html5TypeTagComment
html5TypeDocType
)
const (
html5FlagsDataState = iota
html5FlagsValueNoQuote
html5FlagsValueSingleQuote
html5FlagsValueDoubleQuote
html5FlagsValueBackQuote
)
type fnH5State func() bool
type h5State struct {
s string
len int
pos int
isClose bool
state fnH5State
tokenStart string
tokenLen int
tokenType int
}
-910
View File
@@ -1,910 +0,0 @@
package libinjection
import (
"strings"
)
type sqliState struct {
// input, does not need to be null terminated, it is also not modified.
input string
// length, input length
length int
flags int
// position is the index in the string during tokenization
pos int
// tokenVec, max tokens+1 since we use one extra token to determine the type of the previous token
tokenVec [8]sqliToken
// pointer to token position in tokenVec, above
current *sqliToken
// fingerprint pattern c-string, +1 form ending null
fingerprint string
// |----------------------------------------|
// | |/**/ |--[start] |# |
// |------------|-------|---------------|---|
// |ANSI SQL |ok |ok |no |
// |------------|-------|---------------|---|
// |MYSQL |ok |--[whitespace] |ok |
// |----------------------------------------|
// Number of ddw(dash-dash-white) comments
// These comments are in the form of
// '--[whitespace]' or '--[EOF]'
// All databases treat this as a comment.
// statsCommentDDW int
// Number of ddx(dash-dash-[not white]) comments
//
// ANSI SQL treats these are comments, MYSQL threats this as
// two unary operators '-' '-'
//
// If you are parsing result returns FALSE and
// stats_comment_dd > 0, you should reparse with
// COMMENT_MYSQL
statsCommentDDX int
// c-style comments found /x .. x/
// statsCommentC int
// '#' operators or MYSQL EOL comments found
statsCommentHash int
// number of tokens folded away
statsFolds int
// total tokens processed
statsTokens int
}
func sqliInit(s *sqliState, input string, flags int) {
if flags == 0 {
flags = sqliFlagQuoteNone | sqliFlagSQLAnsi
}
*s = sqliState{}
s.input = input
s.length = len(input)
s.flags = flags
s.current = &s.tokenVec[0]
}
// secondary api: detects SQLi in a string, GIVEN a context.
//
// A context can be:
//
// ByteNull (\0), process as is
// ByteSingle ('), process pretending input started with a
// single quote.
// ByteDouble ("), process pretending input started with a
// double quote.
func (s *sqliState) sqliFingerprint(flags int) string {
s.reset(flags)
length := s.fold()
// check for magic PHP backquote comment
// If:
// last token is of type "bareword"
// And is quoted in a backtick
// And isn't closed
// And it's empty?
// Then convert it to comment
if length > 2 &&
s.tokenVec[length-1].category == sqliTokenTypeBareWord &&
s.tokenVec[length-1].strOpen == byteTick &&
s.tokenVec[length-1].len == 0 &&
s.tokenVec[length-1].strClose == byteNull {
s.tokenVec[length-1].category = sqliTokenTypeComment
}
fp := strings.Builder{}
for i := 0; i < length; i++ {
c := s.tokenVec[i].category
// check for 'X' in pattern, and then
// clear out all tokens
//
// this means parsing could not be done
// accurately due to pgsql's double comments
// or other syntax that isn't consistent.
// Should be very rare false positive
if c == sqliTokenTypeEvil {
s.fingerprint = string(sqliTokenTypeEvil)
s.tokenVec[0].category = sqliTokenTypeEvil
s.tokenVec[0].val = string(sqliTokenTypeEvil)
return s.fingerprint
}
fp.WriteByte(c)
}
s.fingerprint = fp.String()
return s.fingerprint
}
// See if two tokens can be merged since they are compound SQL phrases.
//
// This takes two tokens and if they are the right type,
// merges their values together. Then checks to see if the
// new value is special using the PHRASES mapping.
//
// Example: "UNION" + "ALL" = "UNION ALL"
func (s *sqliState) merge(tokenA, tokenB *sqliToken) bool {
// first token is of right type?
if !(tokenA.category == sqliTokenTypeKeyword ||
tokenA.category == sqliTokenTypeBareWord ||
tokenA.category == sqliTokenTypeOperator ||
tokenA.category == sqliTokenTypeUnion ||
tokenA.category == sqliTokenTypeFunction ||
tokenA.category == sqliTokenTypeExpression ||
tokenA.category == sqliTokenTypeTSQL ||
tokenA.category == sqliTokenTypeSQLType) {
return false
}
if !(tokenB.category == sqliTokenTypeKeyword ||
tokenB.category == sqliTokenTypeBareWord ||
tokenB.category == sqliTokenTypeOperator ||
tokenB.category == sqliTokenTypeUnion ||
tokenB.category == sqliTokenTypeFunction ||
tokenB.category == sqliTokenTypeExpression ||
tokenB.category == sqliTokenTypeTSQL ||
tokenB.category == sqliTokenTypeSQLType ||
tokenB.category == sqliTokenTypeLogicOperator) {
return false
}
// +1 for space in the middle
if tokenA.len+tokenB.len+1 > tokenSize {
// make sure there is room for ending null
return false
}
tmp := tokenA.val[:tokenA.len] + " " + tokenB.val[:tokenB.len]
ch := s.lookupWord(sqliLookupWord, tmp)
if ch != byteNull {
tokenA.assign(ch, tokenA.pos, len(tmp), tmp)
return true
}
return false
}
// parses and folds input, up to 5 tokens
func (s *sqliState) fold() int {
var (
pos = 0 // pos is the position of where whe Next token goes
left = 0 // left is a count of how many tokens that are already folded or processed(i.e. part of the fingerprint)
more = true
lastComment = sqliToken{}
)
s.current = &s.tokenVec[0]
for more {
more = s.tokenize()
if !(s.current.category == sqliTokenTypeComment ||
s.current.category == sqliTokenTypeLeftParenthesis ||
s.current.category == sqliTokenTypeSQLType ||
s.current.isUnaryOp()) {
break
}
}
if !more {
// if input was only comments, unary or (, then exit
return 0
}
// it's some other token
pos++
for {
// do we have all the max number of tokens? if so do
// some special cases for 5 tokens
if pos >= maxTokens {
if (s.tokenVec[0].category == sqliTokenTypeNumber &&
(s.tokenVec[1].category == sqliTokenTypeOperator || s.tokenVec[1].category == sqliTokenTypeComma) &&
s.tokenVec[2].category == sqliTokenTypeLeftParenthesis &&
s.tokenVec[3].category == sqliTokenTypeNumber &&
s.tokenVec[4].category == sqliTokenTypeRightParenthesis) ||
(s.tokenVec[0].category == sqliTokenTypeBareWord &&
s.tokenVec[1].category == sqliTokenTypeOperator &&
s.tokenVec[2].category == sqliTokenTypeLeftParenthesis &&
(s.tokenVec[3].category == sqliTokenTypeBareWord || s.tokenVec[3].category == sqliTokenTypeNumber) &&
s.tokenVec[4].category == sqliTokenTypeRightParenthesis) ||
(s.tokenVec[0].category == sqliTokenTypeNumber &&
s.tokenVec[1].category == sqliTokenTypeRightParenthesis &&
s.tokenVec[2].category == sqliTokenTypeComma &&
s.tokenVec[3].category == sqliTokenTypeLeftParenthesis &&
s.tokenVec[4].category == sqliTokenTypeNumber) ||
(s.tokenVec[0].category == sqliTokenTypeBareWord &&
s.tokenVec[1].category == sqliTokenTypeRightParenthesis &&
s.tokenVec[2].category == sqliTokenTypeOperator &&
s.tokenVec[3].category == sqliTokenTypeLeftParenthesis &&
s.tokenVec[4].category == sqliTokenTypeBareWord) {
if pos > maxTokens {
s.tokenVec[1] = s.tokenVec[5]
pos = 2
left = 0
} else {
pos = 1
left = 0
}
}
}
if !more || left >= maxTokens {
left = pos
break
}
// get up to two tokens
for more && pos <= maxTokens && pos-left < 2 {
s.current = &s.tokenVec[pos]
more = s.tokenize()
if more {
if s.current.category == sqliTokenTypeComment {
lastComment = *s.current
} else {
lastComment.category = byteNull
pos++
}
}
}
// did we get 2 tokens? if not then we are done
if pos-left < 2 {
left = pos
continue
}
// FOLD: "ss" -> "s"
// "foo" "bar" is valid SQL
// just ignore second string
switch {
case s.tokenVec[left].category == sqliTokenTypeString && s.tokenVec[left+1].category == sqliTokenTypeString:
pos--
s.statsFolds++
continue
case s.tokenVec[left].category == sqliTokenTypeSemiColon && s.tokenVec[left+1].category == sqliTokenTypeSemiColon:
// not sure how various engines handle
// 'select 1;;drop table foo' or
// 'select 1;/x foo x/;drop table foo'
// to prevent surprises, just fold away repeated semicolons
pos--
s.statsFolds++
continue
case (s.tokenVec[left].category == sqliTokenTypeOperator || s.tokenVec[left].category == sqliTokenTypeLogicOperator) &&
(s.tokenVec[left+1].isUnaryOp() || s.tokenVec[left+1].category == sqliTokenTypeSQLType):
pos--
s.statsFolds++
left = 0
continue
case s.tokenVec[left].category == sqliTokenTypeLeftParenthesis &&
s.tokenVec[left+1].isUnaryOp():
pos--
s.statsFolds++
if left > 0 {
left--
}
continue
case s.merge(&s.tokenVec[left], &s.tokenVec[left+1]):
pos--
s.statsFolds++
if left > 0 {
left--
}
continue
case s.tokenVec[left].category == sqliTokenTypeSemiColon &&
s.tokenVec[left+1].category == sqliTokenTypeFunction &&
(s.tokenVec[left+1].val[0] == 'I' || s.tokenVec[left+1].val[0] == 'i') &&
(s.tokenVec[left+1].val[1] == 'F' || s.tokenVec[left+1].val[1] == 'f'):
// IF is normally a function, except in Transact-SQL where it can be used as a standalone
// control flow operator, e.g. IF 1=1...
// if found after a semicolon, covert from 'f' type to 'F' type
s.tokenVec[left+1].category = sqliTokenTypeTSQL
// left += 2
// reparse everything, but we probably can advance left, and pos
continue
case (s.tokenVec[left].category == sqliTokenTypeBareWord || s.tokenVec[left].category == sqliTokenTypeVariable) &&
s.tokenVec[left+1].category == sqliTokenTypeLeftParenthesis &&
( // TSQL functions but common enough to be column names
toUpperCmp("USER_ID", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("USER_NAME", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
// Function in MySQL
toUpperCmp("DATABASE", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("PASSWORD", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("USER", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
// MySQL words that act as a variable and are a function
// TSQL current_users is fake_variable
// http://msdn.microsoft.com/en-us/library/ms176050.aspx
toUpperCmp("CURRENT_USER", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("CURRENT_DATE", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("CURRENT_TIME", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("CURRENT_TIMESTAMP", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("LOCALTIME", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("LOCALTIMESTAMP", s.tokenVec[left].val[:s.tokenVec[left].len])):
// pos is the same
// other conversions need to go here... for instance
// password CAN be a function, coalesce CAN be a funtion
s.tokenVec[left].category = sqliTokenTypeFunction
continue
case s.tokenVec[left].category == sqliTokenTypeKeyword &&
(toUpperCmp("IN", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("NOT IN", s.tokenVec[left].val[:s.tokenVec[left].len])):
if s.tokenVec[left+1].category == sqliTokenTypeLeftParenthesis {
// got ... IN ( ... (or 'NOT IN')
// it's an operator
s.tokenVec[left].category = sqliTokenTypeOperator
} else {
// it's nothing
s.tokenVec[left].category = sqliTokenTypeBareWord
}
// "IN" can be used as "IN BOOLEAN MODE" for mysql
// in which case merging of words can be done later
// otherwise it acts as an equality operator __ IN (values..)
//
// here we got "IN" "(" so it's an operator.
// also, back track to handle "NOT IN"
// might need to do the same with like
// two use cases "foo" LIKE "BAR" (normal operator)
// "foo" = LIKE(1,2)
continue
case s.tokenVec[left].category == sqliTokenTypeOperator &&
(toUpperCmp("LIKE", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("NOT LIKE", s.tokenVec[left].val[:s.tokenVec[left].len])):
if s.tokenVec[left+1].category == sqliTokenTypeLeftParenthesis {
// SELECT LIKE(...
// it's a function
s.tokenVec[left].category = sqliTokenTypeFunction
}
case s.tokenVec[left].category == sqliTokenTypeSQLType &&
(s.tokenVec[left+1].category == sqliTokenTypeBareWord ||
s.tokenVec[left+1].category == sqliTokenTypeNumber ||
s.tokenVec[left+1].category == sqliTokenTypeSQLType ||
s.tokenVec[left+1].category == sqliTokenTypeLeftParenthesis ||
s.tokenVec[left+1].category == sqliTokenTypeFunction ||
s.tokenVec[left+1].category == sqliTokenTypeVariable ||
s.tokenVec[left+1].category == sqliTokenTypeString):
s.tokenVec[left] = s.tokenVec[left+1]
pos--
s.statsFolds++
left = 0
continue
case s.tokenVec[left].category == sqliTokenTypeCollate && s.tokenVec[left+1].category == sqliTokenTypeBareWord:
// there are too many collation types.. so if the bareword has a "_"
// then it's TYPE_SQLTYPE
if strings.IndexByte(s.tokenVec[left+1].val[:], '_') != -1 {
s.tokenVec[left+1].category = sqliTokenTypeSQLType
left = 0
}
case s.tokenVec[left].category == sqliTokenTypeBackslash:
if s.tokenVec[left+1].isArithmeticOp() {
// very weird case in TSQL where '\%1' is parsed as '0 % 1', etc.
s.tokenVec[left].category = sqliTokenTypeNumber
} else {
// just ignore it. Again TSQL seems to parse \1 as "1"
s.tokenVec[left] = s.tokenVec[left+1]
pos--
s.statsFolds++
}
left = 0
continue
case s.tokenVec[left].category == sqliTokenTypeLeftParenthesis &&
s.tokenVec[left+1].category == sqliTokenTypeLeftParenthesis:
pos--
left = 0
s.statsFolds++
continue
case s.tokenVec[left].category == sqliTokenTypeRightParenthesis &&
s.tokenVec[left+1].category == sqliTokenTypeRightParenthesis:
pos--
left = 0
s.statsFolds++
continue
case s.tokenVec[left].category == sqliTokenTypeLeftBrace &&
s.tokenVec[left+1].category == sqliTokenTypeBareWord:
// MySQL degenerate case
//
// select { ``.``.id }; -- valid!!
// select { ``.``.``.id }; --invalid
// select ``.``.id; --invalid todo: this is valid
// select { ``.id }; --invalid
//
// so it appears {``.``.id} is a magic case
// I suspect this is "current database, current table, field id"
//
// The folding code can't look at more than 3 tokens, and
// I don't want to make two passes.
//
// Since "{ ``" so rare, we are just going to blacklist it.
//
// Highly likely this will need revisiting!
//
// CREDIT @rsalgado 2013-11-25
if s.tokenVec[left+1].len == 0 {
s.tokenVec[left+1].category = sqliTokenTypeEvil
return left + 2
}
// weird ODBC / MySQL {foo expr} --> expr
// but for this rule we just strip away the "{ foo" part
left = 0
pos -= 2
s.statsFolds += 2
continue
case s.tokenVec[left+1].category == sqliTokenTypeRightBrace:
pos--
left = 0
s.statsFolds++
continue
}
// all cases of handing 2 token is done
// and nothing matched. Get one more token
for more && pos <= maxTokens && pos-left < 3 {
s.current = &s.tokenVec[pos]
more = s.tokenize()
if more {
if s.current.category == sqliTokenTypeComment {
lastComment = *s.current
} else {
lastComment.category = byteNull
pos++
}
}
}
// do we have three tokens? If not then we are done
if pos-left < 3 {
left = pos
continue
}
// now look for three token folding
switch {
case s.tokenVec[left].category == sqliTokenTypeNumber &&
s.tokenVec[left+1].category == sqliTokenTypeOperator &&
s.tokenVec[left+2].category == sqliTokenTypeNumber:
pos -= 2
left = 0
continue
case s.tokenVec[left].category == sqliTokenTypeOperator &&
s.tokenVec[left+1].category != sqliTokenTypeLeftParenthesis &&
s.tokenVec[left+2].category == sqliTokenTypeOperator:
pos -= 2
left = 0
continue
case s.tokenVec[left].category == sqliTokenTypeLogicOperator &&
s.tokenVec[left+2].category == sqliTokenTypeLogicOperator:
pos -= 2
left = 0
continue
case s.tokenVec[left].category == sqliTokenTypeVariable &&
s.tokenVec[left+1].category == sqliTokenTypeOperator &&
(s.tokenVec[left+2].category == sqliTokenTypeVariable ||
s.tokenVec[left+2].category == sqliTokenTypeNumber ||
s.tokenVec[left+2].category == sqliTokenTypeBareWord):
pos -= 2
left = 0
continue
case (s.tokenVec[left].category == sqliTokenTypeBareWord ||
s.tokenVec[left].category == sqliTokenTypeNumber) &&
s.tokenVec[left+1].category == sqliTokenTypeOperator &&
(s.tokenVec[left+2].category == sqliTokenTypeNumber ||
s.tokenVec[left+2].category == sqliTokenTypeBareWord):
pos -= 2
left = 0
continue
case (s.tokenVec[left].category == sqliTokenTypeBareWord ||
s.tokenVec[left].category == sqliTokenTypeNumber ||
s.tokenVec[left].category == sqliTokenTypeVariable ||
s.tokenVec[left].category == sqliTokenTypeString) &&
s.tokenVec[left+1].category == sqliTokenTypeOperator &&
s.tokenVec[left+1].val[:s.tokenVec[left+1].len] == "::" &&
s.tokenVec[left+2].category == sqliTokenTypeSQLType:
pos -= 2
left = 0
s.statsFolds += 2
continue
case (s.tokenVec[left].category == sqliTokenTypeBareWord ||
s.tokenVec[left].category == sqliTokenTypeNumber ||
s.tokenVec[left].category == sqliTokenTypeString ||
s.tokenVec[left].category == sqliTokenTypeVariable) &&
s.tokenVec[left+1].category == sqliTokenTypeComma &&
(s.tokenVec[left+2].category == sqliTokenTypeNumber ||
s.tokenVec[left+2].category == sqliTokenTypeBareWord ||
s.tokenVec[left+2].category == sqliTokenTypeString ||
s.tokenVec[left+2].category == sqliTokenTypeVariable):
pos -= 2
left = 0
continue
case (s.tokenVec[left].category == sqliTokenTypeExpression ||
s.tokenVec[left].category == sqliTokenTypeGroup ||
s.tokenVec[left].category == sqliTokenTypeComma) &&
s.tokenVec[left+1].isUnaryOp() &&
s.tokenVec[left+2].category == sqliTokenTypeLeftParenthesis:
// got something like SELECT + (, LIMIT + (
// remove unary operator
s.tokenVec[left+1] = s.tokenVec[left+2]
pos--
left = 0
continue
case (s.tokenVec[left].category == sqliTokenTypeKeyword ||
s.tokenVec[left].category == sqliTokenTypeExpression ||
s.tokenVec[left].category == sqliTokenTypeGroup) &&
s.tokenVec[left+1].isUnaryOp() &&
(s.tokenVec[left+2].category == sqliTokenTypeNumber ||
s.tokenVec[left+2].category == sqliTokenTypeBareWord ||
s.tokenVec[left+2].category == sqliTokenTypeVariable ||
s.tokenVec[left+2].category == sqliTokenTypeString ||
s.tokenVec[left+2].category == sqliTokenTypeFunction):
// remove unary operators
// select -1
s.tokenVec[left+1] = s.tokenVec[left+2]
pos--
left = 0
continue
case s.tokenVec[left].category == sqliTokenTypeComma &&
s.tokenVec[left+1].isUnaryOp() &&
(s.tokenVec[left+2].category == sqliTokenTypeNumber ||
s.tokenVec[left+2].category == sqliTokenTypeBareWord ||
s.tokenVec[left+2].category == sqliTokenTypeVariable ||
s.tokenVec[left+2].category == sqliTokenTypeString):
// interesting case turn ", -1" --> ",1" PLUS we need to back up
// one token if possible to see if more folding can be done
// "1, -1" --> "1"
s.tokenVec[left+1] = s.tokenVec[left+2]
left = 0
pos -= 3
continue
case s.tokenVec[left].category == sqliTokenTypeComma &&
s.tokenVec[left+1].isUnaryOp() &&
s.tokenVec[left+2].category == sqliTokenTypeFunction:
// Separate case from above since you end up with
// 1,-sin(1) --> 1 (1)
// Here, just do
// 1,-sin(1) --> 1,sin(1)
// just remove unary operator
s.tokenVec[left+1] = s.tokenVec[left+2]
pos--
left = 0
continue
case s.tokenVec[left].category == sqliTokenTypeBareWord &&
s.tokenVec[left+1].category == sqliTokenTypeDot &&
s.tokenVec[left+2].category == sqliTokenTypeBareWord:
// ignore the '.n'
// typically is this database name .table
pos -= 2
left = 0
continue
case s.tokenVec[left].category == sqliTokenTypeExpression &&
s.tokenVec[left+1].category == sqliTokenTypeDot &&
s.tokenVec[left+2].category == sqliTokenTypeBareWord:
// select . `foo` --> select `foo`
s.tokenVec[left+1] = s.tokenVec[left+2]
pos--
left = 0
continue
case s.tokenVec[left].category == sqliTokenTypeFunction &&
s.tokenVec[left+1].category == sqliTokenTypeLeftParenthesis &&
s.tokenVec[left+2].category != sqliTokenTypeRightParenthesis:
// what's going on here
// Some SQL functions like USER() have 0 args
// if we get User(foo), then User is not a function
// This should be expanded since it eliminated a lot of false
// positives.
if toUpperCmp("USER", s.tokenVec[left].val[:s.tokenVec[left].len]) {
s.tokenVec[left].category = sqliTokenTypeBareWord
}
}
// no folding -- assume left-most token is
// good, now use the existing 2 tokens --
// do not get another
left++
}
// if we have 4 or fewer tokens, and we had a comment token
// at the end, add it back
if left < maxTokens && lastComment.category == sqliTokenTypeComment {
s.tokenVec[left] = lastComment
left++
}
// sometimes we grab a 6th token to help
// determine the type of token 5
if left > maxTokens {
left = maxTokens
}
return left
}
func (s *sqliState) tokenize() bool {
if s.length == 0 {
return false
}
*s.current = sqliToken{}
// if we are at beginning of string and in single quote or double quote mode
// then pretend the input starts with a quote
if s.pos == 0 && (s.flags&(sqliFlagQuoteSingle|sqliFlagQuoteDouble)) != 0 {
s.pos = s.current.parseStringCore(s.input, s.length, 0, 0, flag2Delimiter(s.flags))
s.statsTokens++
return true
}
for s.pos < s.length {
// get current character
ch := s.input[s.pos]
// look up the parser, and cell it
s.pos = parseByteFunctions(s, ch)
if s.current.category != byteNull {
s.statsTokens++
return true
}
}
return false
}
// Given a pattern determine if it's a SQLi pattern.
//
// return TRUE if SQLi, false otherwise
func (s *sqliState) blacklist() bool {
length := len(s.fingerprint)
if length < 1 {
return false
}
fp := strings.Builder{}
fp.Grow(length + 1)
fp.WriteByte('0')
for i := 0; i < length; i++ {
ch := s.fingerprint[i]
if ch >= 'a' && ch <= 'z' {
ch -= 0x20
}
fp.WriteByte(ch)
}
return isKeyword(fp.String()) == sqliTokenTypeFingerprint
}
// Given a positive match for a pattern (i.e. pattern is SQLi), this function
// does additional analysis to reduce false positives.
//
// return TRUE if SQLi, false otherwise
func (s *sqliState) notWhitelist() bool {
// We assume we got a SQLi match
// This next part just helps reduce false positives
length := len(s.fingerprint)
if length > 1 && s.fingerprint[length-1] == sqliTokenTypeComment {
// if ending comment is contains 'sp_password' then it's SQLi!
// MS Audit log apparently ignores anything with
// 'sp_password' in it. Unable to find primary reference to
// this "feature" of SQL Server but seems to be known SQLi
// technique
if strings.Contains(s.input, "sp_password") {
return true
}
}
switch length {
case 2:
// case 2 are "very small SQLi" which make them
// hard to tell from normal input...
if s.fingerprint[1] == sqliTokenTypeUnion {
// not sure why but 1U comes up in SQLi attack
// likely part of parameter splitting/etc.
// lots of reasons why "1 union" might be normal
// input, so beep only if other SQLi things are present
//
// it really is a number and 'union'
// otherwise it has folding or comments
return s.statsTokens != 2
}
// if 'comment' is '#' ignore.. too many FP
if s.tokenVec[1].val[0] == '#' {
return false
}
// for fingerprint like 'nc', only comments of /x are treated
// as SQL... ending comments of "--" and "#" are not SQLi
if s.tokenVec[0].category == sqliTokenTypeBareWord &&
s.tokenVec[1].category == sqliTokenTypeComment &&
s.tokenVec[1].val[0] != '/' {
return false
}
// if '1c' ends with '/x' then it's SQLi
if s.tokenVec[0].category == sqliTokenTypeNumber &&
s.tokenVec[1].category == sqliTokenTypeComment &&
s.tokenVec[1].val[0] != '/' {
return true
}
// there are some odd base64-looking query string values
// 1234-ABCDEFEhfhihwuefi--
// which evaluate to "1c"... these are not SQLi
// but 1234-- probably is.
// Make sure the "1" in "1c" is actually a true decimal number
//
// Need to check -original- string since the folding step
// may have merged tokens, e.g. "1+FOO" is folded into "1"
//
// Note: evasion: 1*1--
if s.tokenVec[0].category == sqliTokenTypeNumber &&
s.tokenVec[1].category == sqliTokenTypeComment {
if s.statsTokens > 2 {
// we have some folding going on, highly likely SQLi
return true
}
// we check that next character after the number is either whitespace,
// or '/' or a '-' ==> SQLi
ch := s.input[s.tokenVec[0].len]
if ch <= 32 {
// next char was whitespace,e.g. "1234 --"
// this isn't exactly correct. ideally we should skip over all whitespace
// but this seems to be ok for now
return true
}
if ch == '/' && s.input[s.tokenVec[0].len+1] == '*' {
return true
}
if ch == '-' && s.input[s.tokenVec[0].len+1] == '-' {
return true
}
return false
}
// detect obvious SQLi scans.. many people put '--' in plain text
// so only detect if input ends with '--', e.g. 1-- but not 1-- foo
if s.tokenVec[1].len > 2 && s.tokenVec[1].val[0] == '-' {
return false
}
case 3:
// ...foo' + 'bar...
// no opening quote, no closing quote
// and each string has data
// sos || s&s are string and operator || logic operator and string
switch s.fingerprint {
case "sos", "s&s":
if s.tokenVec[0].strOpen == byteNull &&
s.tokenVec[2].strClose == byteNull &&
s.tokenVec[0].strClose == s.tokenVec[2].strOpen {
// if ...foo" + "bar ...
return true
}
if s.statsTokens == 3 {
return false
}
return false
case "s&n", "n&1", "1&1", "1&v", "1&s":
// 'sexy and 17' not SQLi
// 'sexy and 17<18' SQLi
if s.statsTokens == 3 {
return false
}
}
if s.tokenVec[1].category == sqliTokenTypeKeyword && (s.tokenVec[1].len < 5 || !toUpperCmp("INTO", s.tokenVec[1].val[:4])) {
// if it's not "INTO OUTFILE", or "INTO DUMPFILE" (MySQL)
// then treat as safe
return false
}
}
return true
}
func (s *sqliState) checkFingerprint() bool {
return s.blacklist() && s.notWhitelist()
}
func (s *sqliState) lookupWord(lookupType int, word string) byte {
if lookupType == sqliLookupFingerprint {
if s.checkFingerprint() {
return 'X'
}
return byteNull
}
return searchKeyword(word, sqlKeywords)
}
func (s *sqliState) reset(flags int) {
if flags == 0 {
flags = sqliFlagQuoteNone | sqliFlagSQLAnsi
}
sqliInit(s, s.input, flags)
}
// Main API, detects SQLi in an input
func (s *sqliState) reparseAsMySQL() bool {
return s.statsCommentDDX != 0 || s.statsCommentHash != 0
}
func (s *sqliState) check() bool {
// no input? not SQLi
if s.length == 0 {
return false
}
// test input "as-is"
s.sqliFingerprint(sqliFlagQuoteNone | sqliFlagSQLAnsi)
if s.lookupWord(sqliLookupFingerprint, s.fingerprint) != byteNull {
return true
} else if s.reparseAsMySQL() {
s.sqliFingerprint(sqliFlagQuoteNone | sqliFlagSQLMysql)
if s.lookupWord(sqliLookupFingerprint, s.fingerprint) != byteNull {
return true
}
}
// if input has a single quote, then
// test as if input was actually '
// example: if input if "1' = 1", then pretend it's "'1' = 1"
if strings.IndexByte(s.input, byteSingle) != -1 {
s.sqliFingerprint(sqliFlagQuoteSingle | sqliFlagSQLAnsi)
if s.lookupWord(sqliLookupFingerprint, s.fingerprint) != byteNull {
return true
} else if s.reparseAsMySQL() {
s.sqliFingerprint(sqliFlagQuoteSingle | sqliFlagSQLMysql)
if s.lookupWord(sqliLookupFingerprint, s.fingerprint) != byteNull {
return true
}
}
}
// same as above but with a double quote
if strings.IndexByte(s.input, byteDouble) != -1 {
s.sqliFingerprint(sqliFlagQuoteDouble | sqliFlagSQLMysql)
if s.lookupWord(sqliLookupFingerprint, s.fingerprint) != byteNull {
return true
}
}
// Hurry, input is not SQLi
return false
}
// IsSQLi returns true if the input is SQLi
// It also returns the fingerprint of the SQL Injection as []byte
func IsSQLi(input string) (bool, string) {
state := new(sqliState)
sqliInit(state, input, 0)
result := state.check()
if result {
return result, state.fingerprint
}
return result, ""
}
func IsSQLiNotReturnPrint(input string) bool {
state := new(sqliState)
sqliInit(state, input, 0)
result := state.check()
if result {
return result
}
return result
}
-55
View File
@@ -1,55 +0,0 @@
package libinjection
const (
// sqliFlagNone = 0
sqliFlagQuoteNone = 1
sqliFlagQuoteSingle = 2
sqliFlagQuoteDouble = 4
sqliFlagSQLAnsi = 8
sqliFlagSQLMysql = 16
)
const (
sqliLookupWord = 1
// sqliLookupType = 2
sqliLookupOperator = 3
sqliLookupFingerprint = 4
)
const (
byteNull uint8 = 0
byteSingle uint8 = '\''
byteDouble uint8 = '"'
byteTick uint8 = '`'
)
const (
sqliTokenTypeNone byte = 0
sqliTokenTypeKeyword byte = 'k'
sqliTokenTypeUnion byte = 'U'
sqliTokenTypeGroup byte = 'B'
sqliTokenTypeExpression byte = 'E'
sqliTokenTypeSQLType byte = 't'
sqliTokenTypeFunction byte = 'f'
sqliTokenTypeBareWord byte = 'n'
sqliTokenTypeNumber byte = '1'
sqliTokenTypeVariable byte = 'v'
sqliTokenTypeString byte = 's'
sqliTokenTypeOperator byte = 'o'
sqliTokenTypeLogicOperator byte = '&'
sqliTokenTypeComment byte = 'c'
sqliTokenTypeCollate byte = 'A'
sqliTokenTypeLeftParenthesis byte = '('
sqliTokenTypeRightParenthesis byte = ')'
sqliTokenTypeLeftBrace byte = '{'
sqliTokenTypeRightBrace byte = '}'
sqliTokenTypeDot byte = '.'
sqliTokenTypeComma byte = ','
sqliTokenTypeColon byte = ':'
sqliTokenTypeSemiColon byte = ';'
sqliTokenTypeTSQL byte = 'T'
sqliTokenTypeUnknown byte = '?'
sqliTokenTypeEvil byte = 'X'
sqliTokenTypeFingerprint byte = 'F'
sqliTokenTypeBackslash byte = '\\'
)
File diff suppressed because it is too large Load Diff
-126
View File
@@ -1,126 +0,0 @@
package libinjection
import (
"strings"
)
func flag2Delimiter(flag int) byte {
switch {
case (flag & sqliFlagQuoteSingle) != 0:
return byteSingle
case (flag & sqliFlagQuoteDouble) != 0:
return byteDouble
default:
return byteNull
}
}
// OK! " \" " one backslash = escaped!
//
// " \\" " two backslash = not escaped!
// " \\\" " three backslash = escaped!
func isBackslashEscaped(str string) bool {
if strings.IndexByte(str, '\\') == -1 {
return false
}
var count = 0
for i := len(str) - 1; i >= 0; i-- {
if str[i] == '\\' {
count++
} else {
break
}
}
// if number of backslashes is odd, it is escaped
return count%2 != 0
}
func isDoubleDelimiterEscaped(str string) bool {
return len(str) >= 2 && str[0] == str[1]
}
func isByteWhite(ch byte) bool {
// ' ' space is 0x32
// '\t 0x09 \011 horizontal tab
// '\n' 0x0a \012 new line
// '\v' 0x0b \013 vertical tab
// '\f' 0x0c \014 new page
// '\r' 0x0d \015 carriage return
// 0x00 \000 null (oracle)
// 0xa0 \240 is Latin-1
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\v' || ch == '\f' || ch == '\r' || ch == '\240' || ch == '\000'
}
// Find the largest string containing certain characters.
//
// if accept is "ABC", then this function would be similar to
// regexp.match(str, "[ABC]*")
func strLenSpn(s string, length int, accept string) int {
for i := 0; i < length; i++ {
if strings.IndexByte(accept, s[i]) == -1 {
return i
}
}
return length
}
func strLenCSpn(s string, length int, accept []byte) int {
for i := 0; i < length; i++ {
if accept[s[i]] == 1 {
return i
}
}
return length
}
// This detects MySQL comments, comments that
// start with /x! We just ban these now but
// previously we attempted to parse the inside.
//
// For reference:
// the form of /x![anything]x/ or /x!12345[anything]x/
//
// MySQL3 (maybe 4), allowed this:
//
// /x!0selectx/ 1;
//
// where 0 could be any number
//
// The last version of MySQL 3 was in 2003.
//
// It is unclear if the MySQL 3 syntax was allowed
// in MySQL 4. The last version of MySQL 4 was in 2008.
func isMysqlComment(s string, pos int) bool {
// so far...
// s[pos] == '/' && s[pos+1] == '*'
if pos+2 >= len(s) {
return false
}
if s[pos+2] != '!' {
return false
}
return true
}
func toUpperCmp(a, b string) bool {
return a == strings.ToUpper(b)
}
func isKeyword(key string) byte {
return searchKeyword(key, sqlKeywords)
}
func searchKeyword(key string, keywords map[string]byte) byte {
upperKey := strings.ToUpper(key)
if val, ok := keywords[upperKey]; ok {
return val
}
return byteNull
}
-498
View File
@@ -1,498 +0,0 @@
package libinjection
import (
"bytes"
"strings"
)
var wordAcceptTable = buildAcceptTable(" []{}<>:\\?=@!#~+-*/&|^%(),';\t\n\v\f\r\"\240\000")
var varAcceptTable = buildAcceptTable(" <>:\\?=@!#~+-*/&|^%(),';\t\n\v\f\r'`\"")
func parseEolComment(s *sqliState) int {
index := strings.IndexByte(s.input[s.pos:], '\n')
if index == -1 {
s.current.assign(sqliTokenTypeComment, s.pos, s.length-s.pos, s.input[s.pos:])
return s.length
}
s.current.assign(sqliTokenTypeComment, s.pos, index, s.input[s.pos:])
return s.pos + index + 1
}
func parseMoney(s *sqliState) int {
if s.pos+1 == s.length {
s.current.assign(sqliTokenTypeBareWord, s.pos, 1, "$")
return s.length
}
// $1,000.00 or $1.000,00 ok!
// This also parses $.....,,111 but that's ok
length := strLenSpn(s.input[s.pos+1:], s.length-s.pos-1, "0123456789.,")
switch {
case length == 0:
if s.input[s.pos+1] == '$' {
// we have $$ .. find ending $$ and make string
index := strings.Index(s.input[s.pos+2:], "$$")
if index == -1 {
s.current.assign(sqliTokenTypeString, s.pos+2, s.length-(s.pos+2), s.input[s.pos+2:])
s.current.strOpen = '$'
s.current.strClose = byteNull
return s.length
}
s.current.assign(sqliTokenTypeString, s.pos+2, index, s.input[s.pos+2:])
s.current.strOpen = '$'
s.current.strClose = '$'
return s.pos + 2 + index + 2
}
// ok it's not a number or '$$', but maybe it's pgsql "$ quoted strings"
xlen := strLenSpn(s.input[s.pos+1:], s.length-s.pos-1, "abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
if xlen == 0 {
// hmm, it's "$" _something_ .. just add $ and keep going
s.current.assign(sqliTokenTypeBareWord, s.pos, 1, "$")
return s.pos + 1
}
// we have $foobar?????
if s.pos+xlen+1 == s.length || s.input[s.pos+xlen+1] != '$' {
// not $foobar$, or fell off edge
s.current.assign(sqliTokenTypeBareWord, s.pos, 1, "$")
return s.pos + 1
}
// we have $foobar$ ... find it again
index := strings.Index(s.input[s.pos+xlen+2:], s.input[s.pos:s.pos+xlen+2])
if index == -1 {
s.current.assign(sqliTokenTypeString, s.pos+xlen+2, s.length-s.pos-xlen-2, s.input[s.pos+xlen+2:])
s.current.strOpen = '$'
s.current.strClose = byteNull
return s.length
}
// get one
s.current.assign(sqliTokenTypeString, s.pos+xlen+2, index, s.input[s.pos+xlen+2:])
s.current.strOpen = '$'
s.current.strClose = '$'
return s.pos + xlen + 2 + index + xlen + 2
case length == 1 && s.input[s.pos+1] == '.':
return parseWord(s)
default:
s.current.assign(sqliTokenTypeNumber, s.pos, length+1, s.input[s.pos:])
return s.pos + length + 1
}
}
func parseOther(s *sqliState) int {
s.current.assign(sqliTokenTypeUnknown, s.pos, 1, s.input[s.pos:])
return s.pos + 1
}
func parseWhite(s *sqliState) int {
return s.pos + 1
}
func parseOperator1(s *sqliState) int {
s.current.assign(sqliTokenTypeOperator, s.pos, 1, s.input[s.pos:])
return s.pos + 1
}
func parseByte(s *sqliState) int {
s.current.assign(s.input[s.pos], s.pos, 1, s.input[s.pos:])
return s.pos + 1
}
// In ANSI mode, hash is an operator
// In MYSQL mode, it's a EOL comment like '--'
func parseHash(s *sqliState) int {
s.statsCommentHash++
if (s.flags & sqliFlagSQLMysql) != 0 {
s.statsCommentHash++
return parseEolComment(s)
}
s.current.assign(sqliTokenTypeOperator, s.pos, 1, "#")
return s.pos + 1
}
func parseDash(s *sqliState) int {
// five cases
// 1) --[white] this is always a SQL comment
// 2) --[EOL] this is a comment
// 3) --[not white] in MYSQL this is NOT a comment but two unary operators
// 4) --[not white] everyone else thinks this is a comment
// 5) -[not dash] '-' is a unary operator
switch {
case s.pos+2 < s.length && s.input[s.pos+1] == '-' && isByteWhite(s.input[s.pos+2]):
return parseEolComment(s)
case s.pos+2 == s.length && s.input[s.pos+1] == '-':
return parseEolComment(s)
case s.pos+1 < s.length && s.input[s.pos+1] == '-' && (s.flags&sqliFlagSQLAnsi) != 0:
// --[not white] not white case
s.statsCommentDDX++
return parseEolComment(s)
default:
s.current.assign(sqliTokenTypeOperator, s.pos, 1, "-")
return s.pos + 1
}
}
func parseSlash(s *sqliState) int {
var (
length int
ctype = sqliTokenTypeComment
)
if s.pos+1 == s.length || s.input[s.pos+1] != '*' {
return parseOperator1(s)
}
// skip over initial '/*'
index := strings.Index(s.input[s.pos+2:], "*/")
if index == -1 {
length = s.length - s.pos
} else {
length = 2 + index + 2
}
// postgresql allows nested comments which makes
// which is incompatible with parsing so
// if we find a '/x' inside the comment, then
// make a new token.
//
// Also, Mysql's "conditional" comments for version
// are an automatic black ban!
if index != -1 &&
strings.Contains(s.input[s.pos+2:s.pos+2+index+1], "/*") {
ctype = sqliTokenTypeEvil
} else if isMysqlComment(s.input, s.pos) {
ctype = sqliTokenTypeEvil
}
s.current.assign(ctype, s.pos, length, s.input[s.pos:])
return s.pos + length
}
// weird MySQL alias for NULL, "\N"(capital N only)
func parseBackSlash(s *sqliState) int {
if s.pos+1 < s.length && s.input[s.pos+1] == 'N' {
s.current.assign(sqliTokenTypeNumber, s.pos, 2, s.input[s.pos:])
return s.pos + 2
}
s.current.assign(sqliTokenTypeBackslash, s.pos, 1, s.input[s.pos:])
return s.pos + 1
}
func parseOperator2(s *sqliState) int {
if s.pos+1 >= s.length {
return parseOperator1(s)
}
if s.pos+2 < s.length && s.input[s.pos] == '<' && s.input[s.pos+1] == '=' && s.input[s.pos+2] == '>' {
// special 3-char operator
s.current.assign(sqliTokenTypeOperator, s.pos, 3, s.input[s.pos:])
return s.pos + 3
}
ch := s.lookupWord(sqliLookupOperator, s.input[s.pos:s.pos+2])
if ch != byteNull {
s.current.assign(ch, s.pos, 2, s.input[s.pos:])
return s.pos + 2
}
// not an operator, what to do with the two characters we got?
if s.input[s.pos] == ':' {
// ':' is not an operator
s.current.assign(sqliTokenTypeColon, s.pos, 1, s.input[s.pos:])
return s.pos + 1
}
// must be a single char operator
return parseOperator1(s)
}
// Used when first char is a ' or "
func parseString(s *sqliState) int {
return s.current.parseStringCore(s.input, s.length, s.pos, 1, s.input[s.pos])
}
func parseWord(s *sqliState) int {
length := strLenCSpn(s.input[s.pos:], s.length-s.pos, wordAcceptTable)
s.current.assign(sqliTokenTypeBareWord, s.pos, length, s.input[s.pos:])
// now we need to look inside what we good for "." and "`"
// and see of what is before is a keyword or not
for i := 0; i < s.current.len; i++ {
delimiter := s.current.val[i]
if delimiter == '.' || delimiter == '`' {
ch := s.lookupWord(sqliLookupWord, s.current.val[:i])
if ch != sqliTokenTypeNone && ch != sqliTokenTypeBareWord {
*s.current = sqliToken{}
// we got something like "SELECT.1"
// or SELECT `column`
s.current.assign(ch, s.pos, i, s.input[s.pos:])
return s.pos + i
}
}
}
// do normal lookup with word including '.'
if length < tokenSize {
ch := s.lookupWord(sqliLookupWord, s.current.val[:length])
if ch == byteNull {
ch = sqliTokenTypeBareWord
}
s.current.category = ch
}
return s.pos + length
}
func parseVar(s *sqliState) int {
pos := s.pos + 1
// var count is only used to reconstruct
// the input. It counts the number of '@'
// seen 0 in the case of NULL, 1 or 2
//
// move past optional other '@'
if pos < s.length && s.input[pos] == '@' {
pos++
s.current.count = 2
} else {
s.current.count = 1
}
// MySQL allows @@`version`
if pos < s.length {
if s.input[pos] == '`' {
s.pos = pos
pos = parseTick(s)
s.current.category = sqliTokenTypeVariable
return pos
} else if s.input[pos] == byteSingle || s.input[pos] == byteDouble {
s.pos = pos
pos = parseString(s)
s.current.category = sqliTokenTypeVariable
return pos
}
}
length := strLenCSpn(s.input[pos:], s.length-pos, varAcceptTable)
if length == 0 {
s.current.assign(sqliTokenTypeVariable, pos, 0, s.input[pos:])
return pos
}
s.current.assign(sqliTokenTypeVariable, pos, length, s.input[pos:])
return pos + length
}
func parseNumber(s *sqliState) int {
var (
digits string
haveE int
haveExp int
)
// s.input[s.pos] == '0' has 1/10 chance of being true,
// while s.pos+1 < s.length is almost always true
if s.input[s.pos] == '0' && s.pos+1 < s.length {
if s.input[s.pos+1] == 'X' || s.input[s.pos+1] == 'x' {
digits = "0123456789ABCDEFabcdef"
} else if s.input[s.pos+1] == 'B' || s.input[s.pos+1] == 'b' {
digits = "01"
}
if digits != "" {
length := strLenSpn(s.input[s.pos+2:], s.length-s.pos-2, digits)
if length == 0 {
s.current.assign(sqliTokenTypeBareWord, s.pos, 2, s.input[s.pos:])
return s.pos + 2
}
s.current.assign(sqliTokenTypeNumber, s.pos, 2+length, s.input[s.pos:])
return s.pos + 2 + length
}
}
pos := s.pos
start := s.pos
for pos < s.length && s.input[pos]-'0' <= 9 {
pos++
}
if pos < s.length && s.input[pos] == '.' {
pos++
for pos < s.length && s.input[pos]-'0' <= 9 {
pos++
}
if pos-start == 1 {
// only one character read so far
s.current.assign(sqliTokenTypeDot, start, 1, ".")
return pos
}
}
if pos < s.length {
if s.input[pos] == 'E' || s.input[pos] == 'e' {
haveE = 1
pos++
if pos < s.length && (s.input[pos] == '+' || s.input[pos] == '-') {
pos++
}
for pos < s.length && s.input[pos]-'0' <= 9 {
haveExp = 1
pos++
}
}
}
// oracle's ending float or double suffix
// http://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements003.htm#i139891
if pos < s.length && (s.input[pos] == 'd' || s.input[pos] == 'D' || s.input[pos] == 'f' || s.input[pos] == 'F') {
switch {
case pos+1 == s.length:
// line ends evaluate "... 1.2f$" as '1.2f'
pos++
case isByteWhite(s.input[pos+1]) || s.input[pos+1] == ';':
// easy case, evaluate "... 1.2f ..." as '1.2f'
pos++
case s.input[pos+1] == 'u' || s.input[pos+1] == 'U':
// a bit of a hack but makes '1fUNION' parse as '1f UNION'
pos++
default:
// it's like "123FROM"
// parse as "123" only
}
}
if haveE == 1 && haveExp == 0 {
// very special form of
// "1234.e" "10.10E" ".E" "1e+"
// this is a WORD not a number
s.current.assign(sqliTokenTypeBareWord, start, pos-start, s.input[start:])
} else {
s.current.assign(sqliTokenTypeNumber, start, pos-start, s.input[start:])
}
return pos
}
// MySQL back ticks are a cross between string and a bare word.
func parseTick(s *sqliState) int {
pos := s.current.parseStringCore(s.input, s.length, s.pos, 1, byteTick)
// we could check to see if start and end of
// string are both "`", i.e. make sure we have
// matching set. `foo` vs `foo
// but I don't think it matters much
//
// check value of string to see if it's a keyword,
// function, operator, etc
ch := s.lookupWord(sqliLookupWord, s.current.val[:s.current.len])
if ch == sqliTokenTypeFunction {
// if it's a function, then covert token
s.current.category = sqliTokenTypeFunction
} else {
// otherwise it's a 'n' type -- mysql treats
// everything as a bare word
s.current.category = sqliTokenTypeBareWord
}
return pos
}
func parseUString(s *sqliState) int {
pos := s.pos
if pos+2 < s.length && s.input[pos+1] == '&' && s.input[pos+2] == byteSingle {
s.pos += 2
pos = parseString(s)
s.current.strOpen = 'u'
if s.current.strClose == byteSingle {
s.current.strClose = 'u'
}
return pos
}
return parseWord(s)
}
// Oracle's q string
// https://livesql.oracle.com/apex/livesql/file/content_CIREYU9EA54EOKQ7LAMZKRF6P.html
func parseQString(s *sqliState) int {
return parseQStringCore(s, 0)
}
func parseNqString(s *sqliState) int {
if s.pos+2 < s.length && s.input[s.pos+1] == byteSingle {
return parseEString(s)
}
return parseQStringCore(s, 1)
}
// hex literal string
// re: [xX]'[0123456789abcdefABCDEF]*'
// mysql has requirement if having EVEN number of chars,
// but pgsql does not
func parseXString(s *sqliState) int {
// need at least 2 more characters
// if next char isn't a single quote, then
// continue as a normal word
if s.pos+2 >= s.length || s.input[s.pos+1] != byteSingle {
return parseWord(s)
}
length := strLenSpn(s.input[s.pos+2:], s.length-s.pos-2, "0123456789abcdefABCDEF")
if s.pos+2+length >= s.length || s.input[s.pos+2+length] != byteSingle {
return parseWord(s)
}
s.current.assign(sqliTokenTypeNumber, s.pos, length+3, s.input[s.pos:])
return s.pos + 2 + length + 1
}
// binary literal string
// re: [bB]'[01]*'
func parseBString(s *sqliState) int {
// need at least 3 characters
// if next byte isn't a single quote, then
// continue as normal word
if s.pos+2 >= s.length || s.input[s.pos+1] != byteSingle {
return parseWord(s)
}
length := strLenSpn(s.input[s.pos+2:], s.length-s.pos-2, "01")
if s.pos+2+length >= s.length || s.input[s.pos+2+length] != byteSingle {
return parseWord(s)
}
s.current.assign(sqliTokenTypeNumber, s.pos, length+3, s.input[s.pos:])
return s.pos + 2 + length + 1
}
// used when first byte is E or e:
//
// N or n: mysql "National Character set"
// E : psql "Escaped String"
func parseEString(s *sqliState) int {
if s.pos+2 >= s.length || s.input[s.pos+1] != byteSingle {
return parseWord(s)
}
return s.current.parseStringCore(s.input, s.length, s.pos, 2, byteSingle)
}
// This handles MS SQLSERVER bracket words
// http://stackoverflow.com/questions/3551284/sql-serverwhat-do-brackets-mean-around-column-name
func parseBWord(s *sqliState) int {
end := strings.IndexByte(s.input[s.pos:], ']')
if end == -1 {
s.current.assign(sqliTokenTypeBareWord, s.pos, s.length-s.pos, s.input[s.pos:])
return s.length
}
s.current.assign(sqliTokenTypeBareWord, s.pos, end+1, s.input[s.pos:])
return s.pos + end + 1
}
func buildAcceptTable(acceptStr string) []byte {
accept := []byte(acceptStr)
acceptTable := make([]byte, 256)
for i := 0; i < 256; i++ {
if bytes.IndexByte(accept, byte(i)) != -1 {
acceptTable[i] = 1
}
}
return acceptTable
}
-226
View File
@@ -1,226 +0,0 @@
package libinjection
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
)
func TestIsSQLi(t *testing.T) {
result, fingerprint := IsSQLi("-1' and 1=1 union/* foo */select load_file('/etc/passwd')--")
fmt.Println("=========result==========: ", result)
fmt.Println("=======fingerprint=======: ", string(fingerprint))
}
const (
fingerprints = "fingerprints"
folding = "folding"
tokens = "tokens"
)
func printTokenString(t *sqliToken) string {
out := ""
if t.strOpen != 0 {
out += string(t.strOpen)
}
out += string(t.val[:t.len])
if t.strClose != 0 {
out += string(t.strClose)
}
return out
}
func printToken(t *sqliToken) string {
out := ""
out += string(t.category)
out += " "
switch t.category {
case 's':
out += printTokenString(t)
case 'v':
vc := t.count
if vc == 1 {
out += "@"
} else if vc == 2 {
out += "@@"
}
out += printTokenString(t)
default:
out += string(t.val[:t.len])
}
return strings.TrimSpace(out)
}
func getToken(state *sqliState, i int) *sqliToken {
if i < 0 || i > maxTokens {
panic("token got error!")
}
return &state.tokenVec[i]
}
func readTestData(filename string) map[string]string {
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer f.Close()
var (
data = make(map[string]string)
state = ""
)
br := bufio.NewReaderSize(f, 8192)
for {
line, _, err := br.ReadLine()
if err != nil {
if err == io.EOF {
break
} else {
panic(err)
}
}
str := string(bytes.TrimSpace(line))
if str == "--TEST--" || str == "--INPUT--" || str == "--EXPECTED--" {
state = str
} else {
data[state] += str + "\n"
}
}
data["--TEST--"] = strings.TrimSpace(data["--TEST--"])
data["--INPUT--"] = strings.TrimSpace(data["--INPUT--"])
data["--EXPECTED--"] = strings.TrimSpace(data["--EXPECTED--"])
return data
}
func runSQLiTest(t testing.TB, data map[string]string, filename string, flag string, sqliFlag int) {
t.Helper()
var (
actual = ""
state = new(sqliState)
)
sqliInit(state, data["--INPUT--"], sqliFlag)
switch flag {
case fingerprints:
result, fingerprints := IsSQLi(data["--INPUT--"])
if result {
actual = string(fingerprints[:])
}
case folding:
numTokens := state.fold()
for i := 0; i < numTokens; i++ {
actual += printToken(getToken(state, i)) + "\n"
}
case tokens:
for state.tokenize() {
actual += printToken(state.current) + "\n"
}
}
actual = strings.TrimSpace(actual)
if actual != data["--EXPECTED--"] {
t.Errorf("FILE: (%s)\nINPUT: (%s)\nEXPECTED: (%s)\nGOT: (%s)\n",
filename, data["--INPUT--"], data["--EXPECTED--"], actual)
}
}
func TestSQLiDriver(t *testing.T) {
baseDir := "tests"
dir, err := os.ReadDir(baseDir)
if err != nil {
t.Fatal(err)
}
for _, fi := range dir {
p := filepath.Join(baseDir, fi.Name())
data := readTestData(p)
switch {
case strings.Contains(fi.Name(), "-sqli-"):
runSQLiTest(t, data, p, fingerprints, 0)
case strings.Contains(fi.Name(), "-folding-"):
runSQLiTest(t, data, p, folding, sqliFlagQuoteNone|sqliFlagSQLAnsi)
case strings.Contains(fi.Name(), "-tokens_mysql-"):
runSQLiTest(t, data, p, tokens, sqliFlagQuoteNone|sqliFlagSQLMysql)
case strings.Contains(fi.Name(), "-tokens-"):
runSQLiTest(t, data, p, tokens, sqliFlagQuoteNone|sqliFlagSQLAnsi)
}
}
}
type testCase struct {
name string
data map[string]string
}
func BenchmarkSQLiDriver(b *testing.B) {
baseDir := "./tests/"
dir, err := os.ReadDir(baseDir)
if err != nil {
b.Fatal(err)
}
cases := struct {
sqli []testCase
folding []testCase
tokensMySQL []testCase
tokens []testCase
}{}
for _, fi := range dir {
p := filepath.Join(baseDir, fi.Name())
data := readTestData(p)
tc := testCase{
name: fi.Name(),
data: data,
}
switch {
case strings.Contains(fi.Name(), "-sqli-"):
cases.sqli = append(cases.sqli, tc)
case strings.Contains(fi.Name(), "-folding-"):
cases.folding = append(cases.folding, tc)
case strings.Contains(fi.Name(), "-tokens-"):
cases.tokens = append(cases.tokens, tc)
}
}
b.Run("sqli", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, tc := range cases.sqli {
tt := tc
runSQLiTest(b, tt.data, tt.name, fingerprints, 0)
}
}
})
b.Run("folding", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, tc := range cases.folding {
tt := tc
runSQLiTest(b, tt.data, tt.name, folding, sqliFlagQuoteNone|sqliFlagSQLAnsi)
}
}
})
b.Run("tokens", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, tc := range cases.tokens {
tt := tc
runSQLiTest(b, tt.data, tt.name, tokens, sqliFlagQuoteNone|sqliFlagSQLAnsi)
}
}
})
}
-108
View File
@@ -1,108 +0,0 @@
package libinjection
import "strings"
type sqliToken struct {
// position and length of token in original string
pos int
len int
// count: in type 'v', used for number of opening '@', but maybe used in other contexts
count int
category byte
strOpen byte
strClose byte
val string
}
const (
maxTokens = 5
tokenSize = 32
)
// Look forward for doubling of delimiter
//
// case 'foo''bar' -> foo''bar
//
// ending quote is not duplicated (i.e. escaped)
// since it's the wrong or EOL
func (t *sqliToken) parseStringCore(s string, length, pos, offset int, delimiter byte) int {
// offset is to skip the perhaps first quote char
var (
str = s[pos+offset:]
)
if offset > 0 {
// this is real quote
t.strOpen = delimiter
} else {
// this was a simulated quote
t.strOpen = byteNull
}
for {
index := strings.IndexByte(str, delimiter)
if index != -1 {
str = str[index:]
}
switch {
case index == -1:
// string ended with no trailing quote
// assign what we have
t.assign(sqliTokenTypeString, pos+offset, length-pos-offset, s[pos+offset:])
t.strClose = byteNull
return length
case isBackslashEscaped(s[pos+offset : pos+offset+strings.Index(s[pos+offset:], str)]):
// keep going, move ahead one character
str = str[1:]
continue
case isDoubleDelimiterEscaped(str):
// keep going, move ahead two characters
str = str[2:]
continue
default:
// hey it's a normal string
t.assign(sqliTokenTypeString, pos+offset, len(s[pos+offset:])-len(str), s[pos+offset:])
t.strClose = delimiter
return len(s) - len(str) + 1
}
}
}
func (t *sqliToken) assign(tokenType byte, pos, length int, value string) {
var last int
if length < tokenSize {
last = length
} else {
last = tokenSize - 1
}
t.category = tokenType
t.pos = pos
t.len = last
t.val = value[:last]
}
func (t *sqliToken) isUnaryOp() bool {
if t.category != sqliTokenTypeOperator {
return false
}
switch t.len {
case 1:
return t.val[0] == '+' || t.val[0] == '-' || t.val[0] == '!' || t.val[0] == '~'
case 2:
return t.val[0] == '!' && t.val[1] == '!'
case 3:
return toUpperCmp("NOT", t.val[:3])
default:
return false
}
}
func (t *sqliToken) isArithmeticOp() bool {
return t.category == sqliTokenTypeOperator && t.len == 1 &&
(t.val[0] == '*' || t.val[0] == '/' || t.val[0] == '+' || t.val[0] == '-' || t.val[0] == '%')
}
@@ -1,8 +0,0 @@
--TEST--
strings are merged
--INPUT--
SELECT "first" "second";
--EXPECTED--
E SELECT
s "first"
; ;
@@ -1,8 +0,0 @@
--TEST--
initial unary ops are removed
--INPUT--
- SELECT 1;
--EXPECTED--
E SELECT
1 1
; ;
@@ -1,9 +0,0 @@
--TEST--
initial left-parens are removed
--INPUT--
( SELECT 1 );
--EXPECTED--
E SELECT
1 1
) )
; ;
@@ -1,9 +0,0 @@
--TEST--
initial left-parens are removed
--INPUT--
-( - SELECT 1 );
--EXPECTED--
E SELECT
1 1
) )
; ;
@@ -1,9 +0,0 @@
--TEST--
initial comments removed
--INPUT--
/* foo */ SELECT 1 );
--EXPECTED--
E SELECT
1 1
) )
; ;
@@ -1,9 +0,0 @@
--TEST--
initial comments removed
--INPUT--
- /* foo */ ( /* bar */ -SELECT 1 );
--EXPECTED--
E SELECT
1 1
) )
; ;
@@ -1,5 +0,0 @@
--TEST--
nothing but unary, left-parens, comments
--INPUT--
- /* foo */ ( /* bar */ -
--EXPECTED--
@@ -1,6 +0,0 @@
--TEST--
only one token
--INPUT--
123
--EXPECTED--
1 123
@@ -1,7 +0,0 @@
--TEST--
only two tokens
--INPUT--
123;
--EXPECTED--
1 123
; ;
@@ -1,7 +0,0 @@
--TEST--
two tokens with embedded comment
--INPUT--
123 /* junk */;
--EXPECTED--
1 123
; ;
@@ -1,8 +0,0 @@
--TEST--
two tokens with ending comment
--INPUT--
123; /* junk */
--EXPECTED--
1 123
; ;
c /* junk */
@@ -1,6 +0,0 @@
--TEST--
arithmetic
--INPUT--
-1
--EXPECTED--
1 1
@@ -1,6 +0,0 @@
--TEST--
arithmetic
--INPUT--
1+-1
--EXPECTED--
1 1
@@ -1,6 +0,0 @@
--TEST--
arithmetic
--INPUT--
1+-+1
--EXPECTED--
1 1
@@ -1,7 +0,0 @@
--TEST--
arithmetic
--INPUT--
1+(-1)
--EXPECTED--
1 1
@@ -1,7 +0,0 @@
--TEST--
word and numbers
--INPUT--
1 + foo
--EXPECTED--
1 1
@@ -1,7 +0,0 @@
--TEST--
word and numbers
--INPUT--
foo + 1
--EXPECTED--
n foo
@@ -1,10 +0,0 @@
--TEST--
logical operators folding
--INPUT--
foo OR zap AND bar
--EXPECTED--
n foo
& OR
n bar
@@ -1,11 +0,0 @@
--TEST--
logical operators folding, this unfortunately isn't quite right.
Want this to be "1"
--INPUT--
1+(-(1))
--EXPECTED--
1 1
) )
@@ -1,11 +0,0 @@
--TEST--
logical operators folding
--INPUT--
1 + foo + 1
--EXPECTED--
1 1
@@ -1,6 +0,0 @@
--TEST--
merge backquote words into one (mysql)
--INPUT--
`foo`.`bar`
--EXPECTED--
n foo
@@ -1,8 +0,0 @@
--TEST--
"+n+" -> "+"
--INPUT--
'foo' + zap + 'bar'
--EXPECTED--
s 'foo'
o +
s 'bar'
@@ -1,8 +0,0 @@
--TEST--
three words, no folding
--INPUT--
foo zap bar
--EXPECTED--
n foo
n zap
n bar
@@ -1,5 +0,0 @@
--TEST--
empty!
--INPUT--
--EXPECTED--
@@ -1,6 +0,0 @@
--TEST--
keyword merging test
--INPUT--
UNION
--EXPECTED--
U UNION
@@ -1,6 +0,0 @@
--TEST--
keyword merging test
--INPUT--
UNION ALL
--EXPECTED--
U UNION ALL
@@ -1,6 +0,0 @@
--TEST--
keyword merging test
--INPUT--
UNION /* foo */ALL
--EXPECTED--
U UNION ALL
@@ -1,5 +0,0 @@
--TEST--
empty!
--INPUT--
--EXPECTED--
@@ -1,8 +0,0 @@
--TEST--
keyword unary-operator [1vsnf]
--INPUT--
select - 1;
--EXPECTED--
E select
1 1
; ;
@@ -1,8 +0,0 @@
--TEST--
keyword unary-operator [1vsnf]
--INPUT--
select + 1;
--EXPECTED--
E select
1 1
; ;
@@ -1,8 +0,0 @@
--TEST--
keyword unary-operator [1vsnf]
--INPUT--
select ~ 1;
--EXPECTED--
E select
1 1
; ;
@@ -1,8 +0,0 @@
--TEST--
keyword unary-operator [1vsnf]
--INPUT--
select !! 1;
--EXPECTED--
E select
1 1
; ;
@@ -1,8 +0,0 @@
--TEST--
keyword unary-operator [1vsnf]
--INPUT--
select - @version;
--EXPECTED--
E select
v @version
; ;
@@ -1,8 +0,0 @@
--TEST--
keyword unary-operator [1vsnf]
--INPUT--
select - 'astring';
--EXPECTED--
E select
s 'astring'
; ;
@@ -1,10 +0,0 @@
--TEST--
keyword unary-operator [1vsnf]
--INPUT--
select - sin(1);
--EXPECTED--
E select
f sin
( (
1 1
) )
@@ -1,8 +0,0 @@
--TEST--
keyword unary-operator [1vsnf]
--INPUT--
select - foobar;
--EXPECTED--
E select
n foobar
; ;
@@ -1,8 +0,0 @@
--TEST--
keyword unary-operator [1vsnf]
--INPUT--
select - foobar;
--EXPECTED--
E select
n foobar
; ;
@@ -1,8 +0,0 @@
--TEST--
keywords that can be a function
--INPUT--
select user;
--EXPECTED--
E select
n user
; ;
@@ -1,10 +0,0 @@
--TEST--
keywords that can be a function
--INPUT--
select user();
--EXPECTED--
E select
f user
( (
) )
; ;
@@ -1,8 +0,0 @@
--TEST--
keywords that can be a function
--INPUT--
select password;
--EXPECTED--
E select
n password
; ;
@@ -1,10 +0,0 @@
--TEST--
keywords that can be a function
--INPUT--
select password();
--EXPECTED--
E select
f password
( (
) )
; ;
@@ -1,8 +0,0 @@
--TEST--
keywords that can be a function
--INPUT--
select database;
--EXPECTED--
E select
n database
; ;
@@ -1,10 +0,0 @@
--TEST--
keywords that can be a function
--INPUT--
select database();
--EXPECTED--
E select
f database
( (
) )
; ;
@@ -1,10 +0,0 @@
--TEST--
keywords that can be a function
--INPUT--
select foobar();
--EXPECTED--
E select
n foobar
( (
) )
; ;
@@ -1,9 +0,0 @@
--TEST--
this is a bogus input, using the if function/control-flow token
--INPUT--
select if 1;
--EXPECTED--
E select
f if
1 1
; ;
@@ -1,11 +0,0 @@
--TEST--
if can function as keyword or expression/function depending on database
--INPUT--
select if(1)
--EXPECTED--
E select
f if
( (
1 1
) )
@@ -1,7 +0,0 @@
--TEST--
GitHub Issue 25:
--INPUT--
foo & foo
--EXPECTED--
n foo
@@ -1,7 +0,0 @@
--TEST--
GitHub Issue 25:
--INPUT--
foo foo & foo
--EXPECTED--
n foo
n foo
@@ -1,9 +0,0 @@
--TEST--
pgsql type intents
--INPUT--
SELECT double precision '1';
--EXPECTED--
E SELECT
s '1'
; ;
@@ -1,9 +0,0 @@
--TEST--
pgsql type intents
--INPUT--
SELECT character varying '1';
--EXPECTED--
E SELECT
s '1'
; ;
@@ -1,9 +0,0 @@
--TEST--
pgsql type intents, postfix
--INPUT--
SELECT '1'::money, 2;
--EXPECTED--
E SELECT
s '1'
; ;
@@ -1,9 +0,0 @@
--TEST--
pgsql type intents, postfix
--INPUT--
SELECT float 1;
--EXPECTED--
E SELECT
1 1
; ;
@@ -1,9 +0,0 @@
--TEST--
pgsql type intents, postfix
--INPUT--
SELECT float @@version;
--EXPECTED--
E SELECT
v @@version
; ;
@@ -1,10 +0,0 @@
--TEST--
pgsql type intents, prefix, keyword are not folded
--INPUT--
1 float before
--EXPECTED--
1 1
t float
k before
@@ -1,8 +0,0 @@
--TEST--
pgsql type intents, postfix
--INPUT--
float junk
--EXPECTED--
n junk
@@ -1,8 +0,0 @@
--TEST--
Make sure merged keywords work correct
--INPUT--
1 and 2 not between 3
--EXPECTED--
1 1
& and
1 2
@@ -1,8 +0,0 @@
--TEST--
keywords that can be a function
--INPUT--
select current_user;
--EXPECTED--
E select
v current_user
; ;
@@ -1,10 +0,0 @@
--TEST--
keywords that can be a function
--INPUT--
select current_user();
--EXPECTED--
E select
f current_user
( (
) )
; ;
@@ -1,10 +0,0 @@
--TEST--
keywords that can be a function
--INPUT--
select 1 AND -2 + 3;
--EXPECTED--
E select
1 1
& AND
1 2
; ;
@@ -1,10 +0,0 @@
--TEST--
unary between expression and left-parens is folded away
--INPUT--
select +++ (1);
--EXPECTED--
E select
( (
1 1
) )
; ;
@@ -1,10 +0,0 @@
--TEST--
Not really sql but testing folding
--INPUT--
select 1 / (2,3,4);
--EXPECTED--
E select
1 1
o /
( (
1 2
@@ -1,10 +0,0 @@
--TEST--
IN list handling + folds
--INPUT--
select 1 IN (2,3,4);
--EXPECTED--
E select
1 1
o IN
( (
1 2
@@ -1,10 +0,0 @@
--TEST--
IN list handling + folds
--INPUT--
select 1 not IN (2,3,4);
--EXPECTED--
E select
1 1
o not IN
( (
1 2
@@ -1,10 +0,0 @@
--TEST--
IN list handling + folds
--INPUT--
1 group by -(2)
--EXPECTED--
1 1
B group by
( (
1 2
) )
@@ -1,9 +0,0 @@
--TEST--
IN list handling + folds
--INPUT--
1 group by -2
--EXPECTED--
1 1
B group by
1 2
@@ -1,11 +0,0 @@
--TEST--
repeated semi colons
--INPUT--
1; /* foo */; select 2;
--EXPECTED--
1 1
; ;
E select
1 2
; ;
@@ -1,10 +0,0 @@
--TEST--
union distinct
--INPUT--
1 UNION DISTINCT 1;
--EXPECTED--
1 1
U UNION DISTINCT
1 1
; ;
@@ -1,10 +0,0 @@
--TEST--
union distinct
--INPUT--
1 UNION ALL DISTINCT 1;
--EXPECTED--
1 1
U UNION ALL DISTINCT
1 1
; ;
@@ -1,10 +0,0 @@
--TEST--
union distinct
--INPUT--
1 UNION DISTINCT ALL 1;
--EXPECTED--
1 1
U UNION DISTINCT ALL
1 1
; ;
@@ -1,13 +0,0 @@
--TEST--
select 1,-1;
--INPUT--
select 1,-(2);
--EXPECTED--
E select
1 1
, ,
( (
1 2
@@ -1,10 +0,0 @@
--TEST--
select 1,-1;
--INPUT--
select 1,-1;
--EXPECTED--
E select
1 1
; ;
@@ -1,11 +0,0 @@
--TEST--
if can function as keyword or expression/function depending on database
--INPUT--
1;if 1=1
--EXPECTED--
1 1
; ;
T if
1 1
@@ -1,8 +0,0 @@
--TEST--
test memory problem with negative index
--INPUT--
foo,bar
--EXPECTED--
n foo
@@ -1,10 +0,0 @@
--TEST--
test folding when prefix operator binary is used
--INPUT--
SELECT 1 + binary 1;
--EXPECTED--
E SELECT
1 1
; ;
@@ -1,10 +0,0 @@
--TEST--
test folding when prefix operator binary is used
--INPUT--
SELECT 1 + binary 2 + 3;
--EXPECTED--
E SELECT
1 1
; ;
@@ -1,10 +0,0 @@
--TEST--
weird TSQL bug
--INPUT--
SELECT \%0;
--EXPECTED--
E SELECT
1 \
; ;
@@ -1,10 +0,0 @@
--TEST--
weird TSQL bug
--INPUT--
SELECT \ % 0 ;
--EXPECTED--
E SELECT
1 \
; ;
@@ -1,10 +0,0 @@
--TEST--
weird TSQL bug
--INPUT--
SELECT \1;
--EXPECTED--
E SELECT
1 1
; ;
@@ -1,12 +0,0 @@
--TEST--
automatic detection of 'collation types'
--INPUT--
SELECT 1 COLLATE papers;
--EXPECTED--
E SELECT
1 1
A COLLATE
n papers
; ;
@@ -1,12 +0,0 @@
--TEST--
automatic detection of 'collation types'
--INPUT--
SELECT 1 COLLATE LATIN1_GERMAN2_CI;
--EXPECTED--
E SELECT
1 1
A COLLATE
t LATIN1_GERMAN2_CI
; ;
@@ -1,6 +0,0 @@
--TEST--
folding with operator
--INPUT--
1 mod (2)
--EXPECTED--
1 1
@@ -1,12 +0,0 @@
--TEST--
folding with operator
--INPUT--
@version mod (2)
--EXPECTED--
v @version
o mod
( (
1 2
) )
@@ -1,9 +0,0 @@
--TEST--
folding with variables
--INPUT--
@version + @version
--EXPECTED--
v @version
@@ -1,9 +0,0 @@
--TEST--
folding with variables
--INPUT--
@version + 1
--EXPECTED--
v @version
@@ -1,9 +0,0 @@
--TEST--
folding with variables
--INPUT--
@version + foo
--EXPECTED--
v @version
@@ -1,12 +0,0 @@
--TEST--
folding with left parens
--INPUT--
1))+1
--EXPECTED--
1 1
) )
o +
1 1
@@ -1,12 +0,0 @@
--TEST--
folding with left parens
--INPUT--
1))))))))))))))+1
--EXPECTED--
1 1
) )
o +
1 1
@@ -1,10 +0,0 @@
--TEST--
test of unary operator 'NOT'
--INPUT--
1 + NOT 1
--EXPECTED--
1 1
@@ -1,10 +0,0 @@
--TEST--
test of comma, unary, stuff -> ,
--INPUT--
1,-1
--EXPECTED--
1 1
@@ -1,10 +0,0 @@
--TEST--
test of comma, unary, stuff -> ,
--INPUT--
1,-junk
--EXPECTED--
1 1

Some files were not shown because too many files have changed in this diff Show More