mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-09-24 22:39:38 +08:00
Merge pull request #129 from samwafgo/fix_xss
feat:update libinjection version
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user