mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
123 lines
3.2 KiB
Go
123 lines
3.2 KiB
Go
package datastoretelemetrystore
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/hanzoai/o11y/pkg/errors"
|
|
)
|
|
|
|
// ErrCodeUnsafeStatement is returned when a statement is not a single statement
|
|
// safe to wrap in an EXPLAIN prefix.
|
|
var ErrCodeUnsafeStatement = errors.MustNewCode("unsafe_statement")
|
|
|
|
// scanState is the lexer state while scanning a statement.
|
|
type scanState int
|
|
|
|
const (
|
|
scanNormal scanState = iota
|
|
scanSingle
|
|
scanDouble
|
|
scanBacktick
|
|
scanLineComment
|
|
scanBlockComment
|
|
)
|
|
|
|
// ValidateExplainStatement rejects stacked statements (e.g. `SELECT 1; DROP TABLE t`),
|
|
// the only injection vector left once values are bound and the EXPLAIN prefix is fixed.
|
|
// It scans stmt as Datastore SQL — ignoring ';' inside string literals, quoted
|
|
// identifiers, and comments — and rejects any content after a top-level ';'.
|
|
func ValidateExplainStatement(stmt string) error {
|
|
if strings.TrimSpace(stmt) == "" {
|
|
return errors.NewInvalidInputf(ErrCodeUnsafeStatement, "statement is empty")
|
|
}
|
|
|
|
state := scanNormal
|
|
// terminated is set at a top-level ';'; after it only whitespace, ';', and
|
|
// comments may appear — anything else is a second statement.
|
|
terminated := false
|
|
|
|
for i := 0; i < len(stmt); i++ {
|
|
c := stmt[i]
|
|
switch state {
|
|
case scanNormal:
|
|
if terminated {
|
|
switch {
|
|
case isSQLSpace(c) || c == ';':
|
|
// harmless trailing whitespace / empty statements
|
|
case c == '-' && i+1 < len(stmt) && stmt[i+1] == '-':
|
|
state = scanLineComment
|
|
i++
|
|
case c == '/' && i+1 < len(stmt) && stmt[i+1] == '*':
|
|
state = scanBlockComment
|
|
i++
|
|
default:
|
|
return errors.NewInvalidInputf(ErrCodeUnsafeStatement, "statement must be a single statement; content found after ';'")
|
|
}
|
|
continue
|
|
}
|
|
switch {
|
|
case c == '\'':
|
|
state = scanSingle
|
|
case c == '"':
|
|
state = scanDouble
|
|
case c == '`':
|
|
state = scanBacktick
|
|
case c == '-' && i+1 < len(stmt) && stmt[i+1] == '-':
|
|
state = scanLineComment
|
|
i++
|
|
case c == '/' && i+1 < len(stmt) && stmt[i+1] == '*':
|
|
state = scanBlockComment
|
|
i++
|
|
case c == ';':
|
|
terminated = true
|
|
}
|
|
case scanSingle:
|
|
i = skipQuoted(stmt, i, '\'', &state)
|
|
case scanDouble:
|
|
i = skipQuoted(stmt, i, '"', &state)
|
|
case scanBacktick:
|
|
i = skipQuoted(stmt, i, '`', &state)
|
|
case scanLineComment:
|
|
if c == '\n' {
|
|
state = scanNormal
|
|
}
|
|
case scanBlockComment:
|
|
if c == '*' && i+1 < len(stmt) && stmt[i+1] == '/' {
|
|
state = scanNormal
|
|
i++
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// skipQuoted advances one character within a quoted literal/identifier delimited
|
|
// by quote. A backslash or doubled quote escapes; an unescaped quote ends the
|
|
// literal (resetting *state). It returns the index to resume from (caller adds one).
|
|
func skipQuoted(s string, i int, quote byte, state *scanState) int {
|
|
c := s[i]
|
|
switch c {
|
|
case '\\':
|
|
// skip the escaped character
|
|
return i + 1
|
|
case quote:
|
|
if i+1 < len(s) && s[i+1] == quote {
|
|
// doubled quote: stay inside the literal
|
|
return i + 1
|
|
}
|
|
*state = scanNormal
|
|
}
|
|
return i
|
|
}
|
|
|
|
// isSQLSpace reports whether c is SQL statement whitespace.
|
|
func isSQLSpace(c byte) bool {
|
|
switch c {
|
|
case ' ', '\t', '\n', '\r', '\v', '\f':
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|