Frontend Labs
⋯

Day 69 — SQL Injection Detection

Learning Objectives

  • Implement SQLi detection with multiple techniques: keywords, quote balance, comment patterns
  • Tune for zero false positives on 15 legitimate inputs
  • Understand why WAF-only SQLi defense is insufficient (defense-in-depth)

Key Concepts

SQLi Attack Categories:
  Classic:    ' OR '1'='1   (breaks out of string context)
  Union:      ' UNION SELECT username,password FROM users--
  Blind:      ' AND 1=1--  / ' AND 1=2-- (boolean-based)
  Time-based: ' AND SLEEP(5)-- (detects via response time)
  Stacked:    '; DROP TABLE users--

Common Evasion Techniques (why normalization matters):
  Case:       SeLeCt, sElEcT
  Comments:   SE/*comment*/LECT, UN/**/ION
  URL encode: %53%45%4C%45%43%54
  Whitespace: SELECT\t\nFROM (tab/newline instead of space)

Exercise: SQLi Detection Rule

package waf

import (
    "net/http"
    "regexp"
    "strings"
    "unicode"
)

// Pre-compiled at package init — NEVER inside Match()
var (
    // Keywords that appear in SQLi but rarely in legitimate data
    sqliKeywordsRe = regexp.MustCompile(`(?i)\b(UNION\s+(?:ALL\s+)?SELECT|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM|DROP\s+(?:TABLE|DATABASE)|CREATE\s+(?:TABLE|DATABASE|USER)|EXEC(?:UTE)?|CAST\s*\(|CONVERT\s*\(|INFORMATION_SCHEMA|SLEEP\s*\(|BENCHMARK\s*\(|LOAD_FILE\s*\(|INTO\s+OUTFILE)\b`)

    // Comment patterns used in SQLi
    sqliCommentRe = regexp.MustCompile(`(--|#|\/\*.*?\*\/)`)

    // Dangerous tautologies
    sqliTautologyRe = regexp.MustCompile(`(?i)'\s*(?:OR|AND)\s+['"]?[0-9a-z]+['"]?\s*=\s*['"]?[0-9a-z]+['"]?`)
)

type SQLiRule struct{}

func (r *SQLiRule) ID() string       { return "SQLI-001" }
func (r *SQLiRule) Category() string { return "SQLI" }

func (r *SQLiRule) Match(req *http.Request, body []byte) (bool, int, string) {
    inputs := extractInputs(req, body)
    for _, input := range inputs {
        normalized := normalizeSQLi(input)

        // Check 1: keywords (score: 5)
        if m := sqliKeywordsRe.FindString(normalized); m != "" {
            return true, 5, fmt.Sprintf("SQLi keyword: %q", m)
        }

        // Check 2: tautology patterns (score: 8)
        if m := sqliTautologyRe.FindString(normalized); m != "" {
            return true, 8, fmt.Sprintf("SQLi tautology: %q", m)
        }

        // Check 3: unbalanced quotes + SQL comment (score: 7)
        stripped := sqliCommentRe.ReplaceAllString(normalized, "")
        if unbalancedQuotes(stripped) && sqliCommentRe.MatchString(normalized) {
            return true, 7, "SQLi: unbalanced quotes with comment"
        }
    }
    return false, 0, ""
}

// normalizeSQLi prepares input for rule matching
func normalizeSQLi(input string) string {
    // URL decode
    decoded, _ := url.QueryUnescape(input)
    // Collapse whitespace (including tabs, newlines)
    decoded = strings.Map(func(r rune) rune {
        if unicode.IsSpace(r) { return ' ' }
        return r
    }, decoded)
    // Collapse multiple spaces
    for strings.Contains(decoded, "  ") {
        decoded = strings.ReplaceAll(decoded, "  ", " ")
    }
    return strings.TrimSpace(decoded)
}

// unbalancedQuotes returns true if single quotes are not balanced
func unbalancedQuotes(s string) bool {
    count := 0
    for _, c := range s {
        if c == '\'' { count++ }
    }
    return count%2 != 0
}

// Test with these payloads (all should trigger):
var sqliTestPayloads = []string{
    "' OR '1'='1",
    "1' UNION SELECT * FROM users--",
    "admin'--",
    "1; DROP TABLE users--",
    "' OR 1=1#",
    "' UNION ALL SELECT NULL,NULL,NULL--",
    "1 AND SLEEP(5)--",
    "1' AND EXTRACTVALUE(1,CONCAT(0x7e,(SELECT version())))--",
}

// Legitimate inputs that must NOT trigger (tune to 0 false positives):
var sqliLegitimateInputs = []string{
    "O'Brien",                   // Irish name with apostrophe
    "It's a great product",      // natural language apostrophe
    "SELECT from dropdown",      // SELECT in natural language
    "DROP shipping address",     // DROP in context
    "2024-01-01",               // date
    "user@example.com",         // email
    "Product #123",             // hash in context
    "price > 100 AND category = 'electronics'", // URL query params
}

Real-World Context

SQLi is still #3 in OWASP Top 10 despite 20 years of awareness. The 2017 Equifax breach was SQL injection. WAF as the last line of defense is correct — parameterized queries are the real fix, but WAF catches application bugs. The normalizeSQLi function is why ModSecurity has hundreds of normalization rules.


Generated from week10.md · 8 Oct 2026 16:36