| // Copyright 2023 The go-fuzz-headers Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package gofuzzheaders |
| |
| import ( |
| "fmt" |
| "strings" |
| ) |
| |
| // returns a keyword by index |
| func getKeyword(f *ConsumeFuzzer) (string, error) { |
| index, err := f.GetInt() |
| if err != nil { |
| return keywords[0], err |
| } |
| for i, k := range keywords { |
| if i == index { |
| return k, nil |
| } |
| } |
| return keywords[0], fmt.Errorf("could not get a kw") |
| } |
| |
| // Simple utility function to check if a string |
| // slice contains a string. |
| func containsString(s []string, e string) bool { |
| for _, a := range s { |
| if a == e { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // These keywords are used specifically for fuzzing Vitess |
| var keywords = []string{ |
| "accessible", "action", "add", "after", "against", "algorithm", |
| "all", "alter", "always", "analyze", "and", "as", "asc", "asensitive", |
| "auto_increment", "avg_row_length", "before", "begin", "between", |
| "bigint", "binary", "_binary", "_utf8mb4", "_utf8", "_latin1", "bit", |
| "blob", "bool", "boolean", "both", "by", "call", "cancel", "cascade", |
| "cascaded", "case", "cast", "channel", "change", "char", "character", |
| "charset", "check", "checksum", "coalesce", "code", "collate", "collation", |
| "column", "columns", "comment", "committed", "commit", "compact", "complete", |
| "compressed", "compression", "condition", "connection", "constraint", "continue", |
| "convert", "copy", "cume_dist", "substr", "substring", "create", "cross", |
| "csv", "current_date", "current_time", "current_timestamp", "current_user", |
| "cursor", "data", "database", "databases", "day", "day_hour", "day_microsecond", |
| "day_minute", "day_second", "date", "datetime", "dec", "decimal", "declare", |
| "default", "definer", "delay_key_write", "delayed", "delete", "dense_rank", |
| "desc", "describe", "deterministic", "directory", "disable", "discard", |
| "disk", "distinct", "distinctrow", "div", "double", "do", "drop", "dumpfile", |
| "duplicate", "dynamic", "each", "else", "elseif", "empty", "enable", |
| "enclosed", "encryption", "end", "enforced", "engine", "engines", "enum", |
| "error", "escape", "escaped", "event", "exchange", "exclusive", "exists", |
| "exit", "explain", "expansion", "export", "extended", "extract", "false", |
| "fetch", "fields", "first", "first_value", "fixed", "float", "float4", |
| "float8", "flush", "for", "force", "foreign", "format", "from", "full", |
| "fulltext", "function", "general", "generated", "geometry", "geometrycollection", |
| "get", "global", "gtid_executed", "grant", "group", "grouping", "groups", |
| "group_concat", "having", "header", "high_priority", "hosts", "hour", "hour_microsecond", |
| "hour_minute", "hour_second", "if", "ignore", "import", "in", "index", "indexes", |
| "infile", "inout", "inner", "inplace", "insensitive", "insert", "insert_method", |
| "int", "int1", "int2", "int3", "int4", "int8", "integer", "interval", |
| "into", "io_after_gtids", "is", "isolation", "iterate", "invoker", "join", |
| "json", "json_table", "key", "keys", "keyspaces", "key_block_size", "kill", "lag", |
| "language", "last", "last_value", "last_insert_id", "lateral", "lead", "leading", |
| "leave", "left", "less", "level", "like", "limit", "linear", "lines", |
| "linestring", "load", "local", "localtime", "localtimestamp", "lock", "logs", |
| "long", "longblob", "longtext", "loop", "low_priority", "manifest", |
| "master_bind", "match", "max_rows", "maxvalue", "mediumblob", "mediumint", |
| "mediumtext", "memory", "merge", "microsecond", "middleint", "min_rows", "minute", |
| "minute_microsecond", "minute_second", "mod", "mode", "modify", "modifies", |
| "multilinestring", "multipoint", "multipolygon", "month", "name", |
| "names", "natural", "nchar", "next", "no", "none", "not", "no_write_to_binlog", |
| "nth_value", "ntile", "null", "numeric", "of", "off", "offset", "on", |
| "only", "open", "optimize", "optimizer_costs", "option", "optionally", |
| "or", "order", "out", "outer", "outfile", "over", "overwrite", "pack_keys", |
| "parser", "partition", "partitioning", "password", "percent_rank", "plugins", |
| "point", "polygon", "precision", "primary", "privileges", "processlist", |
| "procedure", "query", "quarter", "range", "rank", "read", "reads", "read_write", |
| "real", "rebuild", "recursive", "redundant", "references", "regexp", "relay", |
| "release", "remove", "rename", "reorganize", "repair", "repeat", "repeatable", |
| "replace", "require", "resignal", "restrict", "return", "retry", "revert", |
| "revoke", "right", "rlike", "rollback", "row", "row_format", "row_number", |
| "rows", "s3", "savepoint", "schema", "schemas", "second", "second_microsecond", |
| "security", "select", "sensitive", "separator", "sequence", "serializable", |
| "session", "set", "share", "shared", "show", "signal", "signed", "slow", |
| "smallint", "spatial", "specific", "sql", "sqlexception", "sqlstate", |
| "sqlwarning", "sql_big_result", "sql_cache", "sql_calc_found_rows", |
| "sql_no_cache", "sql_small_result", "ssl", "start", "starting", |
| "stats_auto_recalc", "stats_persistent", "stats_sample_pages", "status", |
| "storage", "stored", "straight_join", "stream", "system", "vstream", |
| "table", "tables", "tablespace", "temporary", "temptable", "terminated", |
| "text", "than", "then", "time", "timestamp", "timestampadd", "timestampdiff", |
| "tinyblob", "tinyint", "tinytext", "to", "trailing", "transaction", "tree", |
| "traditional", "trigger", "triggers", "true", "truncate", "uncommitted", |
| "undefined", "undo", "union", "unique", "unlock", "unsigned", "update", |
| "upgrade", "usage", "use", "user", "user_resources", "using", "utc_date", |
| "utc_time", "utc_timestamp", "validation", "values", "variables", "varbinary", |
| "varchar", "varcharacter", "varying", "vgtid_executed", "virtual", "vindex", |
| "vindexes", "view", "vitess", "vitess_keyspaces", "vitess_metadata", |
| "vitess_migration", "vitess_migrations", "vitess_replication_status", |
| "vitess_shards", "vitess_tablets", "vschema", "warnings", "when", |
| "where", "while", "window", "with", "without", "work", "write", "xor", |
| "year", "year_month", "zerofill", |
| } |
| |
| // Keywords that could get an additional keyword |
| var needCustomString = []string{ |
| "DISTINCTROW", "FROM", // Select keywords: |
| "GROUP BY", "HAVING", "WINDOW", |
| "FOR", |
| "ORDER BY", "LIMIT", |
| "INTO", "PARTITION", "AS", // Insert Keywords: |
| "ON DUPLICATE KEY UPDATE", |
| "WHERE", "LIMIT", // Delete keywords |
| "INFILE", "INTO TABLE", "CHARACTER SET", // Load keywords |
| "TERMINATED BY", "ENCLOSED BY", |
| "ESCAPED BY", "STARTING BY", |
| "TERMINATED BY", "STARTING BY", |
| "IGNORE", |
| "VALUE", "VALUES", // Replace tokens |
| "SET", // Update tokens |
| "ENGINE =", // Drop tokens |
| "DEFINER =", "ON SCHEDULE", "RENAME TO", // Alter tokens |
| "COMMENT", "DO", "INITIAL_SIZE = ", "OPTIONS", |
| } |
| |
| var alterTableTokens = [][]string{ |
| {"CUSTOM_FUZZ_STRING"}, |
| {"CUSTOM_ALTTER_TABLE_OPTIONS"}, |
| {"PARTITION_OPTIONS_FOR_ALTER_TABLE"}, |
| } |
| |
| var alterTokens = [][]string{ |
| { |
| "DATABASE", "SCHEMA", "DEFINER = ", "EVENT", "FUNCTION", "INSTANCE", |
| "LOGFILE GROUP", "PROCEDURE", "SERVER", |
| }, |
| {"CUSTOM_FUZZ_STRING"}, |
| { |
| "ON SCHEDULE", "ON COMPLETION PRESERVE", "ON COMPLETION NOT PRESERVE", |
| "ADD UNDOFILE", "OPTIONS", |
| }, |
| {"RENAME TO", "INITIAL_SIZE = "}, |
| {"ENABLE", "DISABLE", "DISABLE ON SLAVE", "ENGINE"}, |
| {"COMMENT"}, |
| {"DO"}, |
| } |
| |
| var setTokens = [][]string{ |
| {"CHARACTER SET", "CHARSET", "CUSTOM_FUZZ_STRING", "NAMES"}, |
| {"CUSTOM_FUZZ_STRING", "DEFAULT", "="}, |
| {"CUSTOM_FUZZ_STRING"}, |
| } |
| |
| var dropTokens = [][]string{ |
| {"TEMPORARY", "UNDO"}, |
| { |
| "DATABASE", "SCHEMA", "EVENT", "INDEX", "LOGFILE GROUP", |
| "PROCEDURE", "FUNCTION", "SERVER", "SPATIAL REFERENCE SYSTEM", |
| "TABLE", "TABLESPACE", "TRIGGER", "VIEW", |
| }, |
| {"IF EXISTS"}, |
| {"CUSTOM_FUZZ_STRING"}, |
| {"ON", "ENGINE = ", "RESTRICT", "CASCADE"}, |
| } |
| |
| var renameTokens = [][]string{ |
| {"TABLE"}, |
| {"CUSTOM_FUZZ_STRING"}, |
| {"TO"}, |
| {"CUSTOM_FUZZ_STRING"}, |
| } |
| |
| var truncateTokens = [][]string{ |
| {"TABLE"}, |
| {"CUSTOM_FUZZ_STRING"}, |
| } |
| |
| var createTokens = [][]string{ |
| {"OR REPLACE", "TEMPORARY", "UNDO"}, // For create spatial reference system |
| { |
| "UNIQUE", "FULLTEXT", "SPATIAL", "ALGORITHM = UNDEFINED", "ALGORITHM = MERGE", |
| "ALGORITHM = TEMPTABLE", |
| }, |
| { |
| "DATABASE", "SCHEMA", "EVENT", "FUNCTION", "INDEX", "LOGFILE GROUP", |
| "PROCEDURE", "SERVER", "SPATIAL REFERENCE SYSTEM", "TABLE", "TABLESPACE", |
| "TRIGGER", "VIEW", |
| }, |
| {"IF NOT EXISTS"}, |
| {"CUSTOM_FUZZ_STRING"}, |
| } |
| |
| /* |
| // For future use. |
| var updateTokens = [][]string{ |
| {"LOW_PRIORITY"}, |
| {"IGNORE"}, |
| {"SET"}, |
| {"WHERE"}, |
| {"ORDER BY"}, |
| {"LIMIT"}, |
| } |
| */ |
| |
| var replaceTokens = [][]string{ |
| {"LOW_PRIORITY", "DELAYED"}, |
| {"INTO"}, |
| {"PARTITION"}, |
| {"CUSTOM_FUZZ_STRING"}, |
| {"VALUES", "VALUE"}, |
| } |
| |
| var loadTokens = [][]string{ |
| {"DATA"}, |
| {"LOW_PRIORITY", "CONCURRENT", "LOCAL"}, |
| {"INFILE"}, |
| {"REPLACE", "IGNORE"}, |
| {"INTO TABLE"}, |
| {"PARTITION"}, |
| {"CHARACTER SET"}, |
| {"FIELDS", "COLUMNS"}, |
| {"TERMINATED BY"}, |
| {"OPTIONALLY"}, |
| {"ENCLOSED BY"}, |
| {"ESCAPED BY"}, |
| {"LINES"}, |
| {"STARTING BY"}, |
| {"TERMINATED BY"}, |
| {"IGNORE"}, |
| {"LINES", "ROWS"}, |
| {"CUSTOM_FUZZ_STRING"}, |
| } |
| |
| // These Are everything that comes after "INSERT" |
| var insertTokens = [][]string{ |
| {"LOW_PRIORITY", "DELAYED", "HIGH_PRIORITY", "IGNORE"}, |
| {"INTO"}, |
| {"PARTITION"}, |
| {"CUSTOM_FUZZ_STRING"}, |
| {"AS"}, |
| {"ON DUPLICATE KEY UPDATE"}, |
| } |
| |
| // These are everything that comes after "SELECT" |
| var selectTokens = [][]string{ |
| {"*", "CUSTOM_FUZZ_STRING", "DISTINCTROW"}, |
| {"HIGH_PRIORITY"}, |
| {"STRAIGHT_JOIN"}, |
| {"SQL_SMALL_RESULT", "SQL_BIG_RESULT", "SQL_BUFFER_RESULT"}, |
| {"SQL_NO_CACHE", "SQL_CALC_FOUND_ROWS"}, |
| {"CUSTOM_FUZZ_STRING"}, |
| {"FROM"}, |
| {"WHERE"}, |
| {"GROUP BY"}, |
| {"HAVING"}, |
| {"WINDOW"}, |
| {"ORDER BY"}, |
| {"LIMIT"}, |
| {"CUSTOM_FUZZ_STRING"}, |
| {"FOR"}, |
| } |
| |
| // These are everything that comes after "DELETE" |
| var deleteTokens = [][]string{ |
| {"LOW_PRIORITY", "QUICK", "IGNORE", "FROM", "AS"}, |
| {"PARTITION"}, |
| {"WHERE"}, |
| {"ORDER BY"}, |
| {"LIMIT"}, |
| } |
| |
| var alter_table_options = []string{ |
| "ADD", "COLUMN", "FIRST", "AFTER", "INDEX", "KEY", "FULLTEXT", "SPATIAL", |
| "CONSTRAINT", "UNIQUE", "FOREIGN KEY", "CHECK", "ENFORCED", "DROP", "ALTER", |
| "NOT", "INPLACE", "COPY", "SET", "VISIBLE", "INVISIBLE", "DEFAULT", "CHANGE", |
| "CHARACTER SET", "COLLATE", "DISABLE", "ENABLE", "KEYS", "TABLESPACE", "LOCK", |
| "FORCE", "MODIFY", "SHARED", "EXCLUSIVE", "NONE", "ORDER BY", "RENAME COLUMN", |
| "AS", "=", "ASC", "DESC", "WITH", "WITHOUT", "VALIDATION", "ADD PARTITION", |
| "DROP PARTITION", "DISCARD PARTITION", "IMPORT PARTITION", "TRUNCATE PARTITION", |
| "COALESCE PARTITION", "REORGANIZE PARTITION", "EXCHANGE PARTITION", |
| "ANALYZE PARTITION", "CHECK PARTITION", "OPTIMIZE PARTITION", "REBUILD PARTITION", |
| "REPAIR PARTITION", "REMOVE PARTITIONING", "USING", "BTREE", "HASH", "COMMENT", |
| "KEY_BLOCK_SIZE", "WITH PARSER", "AUTOEXTEND_SIZE", "AUTO_INCREMENT", "AVG_ROW_LENGTH", |
| "CHECKSUM", "INSERT_METHOD", "ROW_FORMAT", "DYNAMIC", "FIXED", "COMPRESSED", "REDUNDANT", |
| "COMPACT", "SECONDARY_ENGINE_ATTRIBUTE", "STATS_AUTO_RECALC", "STATS_PERSISTENT", |
| "STATS_SAMPLE_PAGES", "ZLIB", "LZ4", "ENGINE_ATTRIBUTE", "KEY_BLOCK_SIZE", "MAX_ROWS", |
| "MIN_ROWS", "PACK_KEYS", "PASSWORD", "COMPRESSION", "CONNECTION", "DIRECTORY", |
| "DELAY_KEY_WRITE", "ENCRYPTION", "STORAGE", "DISK", "MEMORY", "UNION", |
| } |
| |
| // Creates an 'alter table' statement. 'alter table' is an exception |
| // in that it has its own function. The majority of statements |
| // are created by 'createStmt()'. |
| func createAlterTableStmt(f *ConsumeFuzzer) (string, error) { |
| maxArgs, err := f.GetInt() |
| if err != nil { |
| return "", err |
| } |
| maxArgs = maxArgs % 30 |
| if maxArgs == 0 { |
| return "", fmt.Errorf("could not create alter table stmt") |
| } |
| |
| var stmt strings.Builder |
| stmt.WriteString("ALTER TABLE ") |
| for i := 0; i < maxArgs; i++ { |
| // Calculate if we get existing token or custom string |
| tokenType, err := f.GetInt() |
| if err != nil { |
| return "", err |
| } |
| if tokenType%4 == 1 { |
| customString, err := f.GetString() |
| if err != nil { |
| return "", err |
| } |
| stmt.WriteString(" " + customString) |
| } else { |
| tokenIndex, err := f.GetInt() |
| if err != nil { |
| return "", err |
| } |
| stmt.WriteString(" " + alter_table_options[tokenIndex%len(alter_table_options)]) |
| } |
| } |
| return stmt.String(), nil |
| } |
| |
| func chooseToken(tokens []string, f *ConsumeFuzzer) (string, error) { |
| index, err := f.GetInt() |
| if err != nil { |
| return "", err |
| } |
| var token strings.Builder |
| token.WriteString(tokens[index%len(tokens)]) |
| if token.String() == "CUSTOM_FUZZ_STRING" { |
| customFuzzString, err := f.GetString() |
| if err != nil { |
| return "", err |
| } |
| return customFuzzString, nil |
| } |
| |
| // Check if token requires an argument |
| if containsString(needCustomString, token.String()) { |
| customFuzzString, err := f.GetString() |
| if err != nil { |
| return "", err |
| } |
| token.WriteString(" " + customFuzzString) |
| } |
| return token.String(), nil |
| } |
| |
| var stmtTypes = map[string][][]string{ |
| "DELETE": deleteTokens, |
| "INSERT": insertTokens, |
| "SELECT": selectTokens, |
| "LOAD": loadTokens, |
| "REPLACE": replaceTokens, |
| "CREATE": createTokens, |
| "DROP": dropTokens, |
| "RENAME": renameTokens, |
| "TRUNCATE": truncateTokens, |
| "SET": setTokens, |
| "ALTER": alterTokens, |
| "ALTER TABLE": alterTableTokens, // ALTER TABLE has its own set of tokens |
| } |
| |
| var stmtTypeEnum = map[int]string{ |
| 0: "DELETE", |
| 1: "INSERT", |
| 2: "SELECT", |
| 3: "LOAD", |
| 4: "REPLACE", |
| 5: "CREATE", |
| 6: "DROP", |
| 7: "RENAME", |
| 8: "TRUNCATE", |
| 9: "SET", |
| 10: "ALTER", |
| 11: "ALTER TABLE", |
| } |
| |
| func createStmt(f *ConsumeFuzzer) (string, error) { |
| stmtIndex, err := f.GetInt() |
| if err != nil { |
| return "", err |
| } |
| stmtIndex = stmtIndex % len(stmtTypes) |
| |
| queryType := stmtTypeEnum[stmtIndex] |
| tokens := stmtTypes[queryType] |
| |
| // We have custom creator for ALTER TABLE |
| if queryType == "ALTER TABLE" { |
| query, err := createAlterTableStmt(f) |
| if err != nil { |
| return "", err |
| } |
| return query, nil |
| } |
| |
| // Here we are creating a query that is not |
| // an 'alter table' query. For available |
| // queries, see "stmtTypes" |
| |
| // First specify the first query keyword: |
| var query strings.Builder |
| query.WriteString(queryType) |
| |
| // Next create the args for the |
| queryArgs, err := createStmtArgs(tokens, f) |
| if err != nil { |
| return "", err |
| } |
| query.WriteString(" " + queryArgs) |
| return query.String(), nil |
| } |
| |
| // Creates the arguments of a statements. In a select statement |
| // that would be everything after "select". |
| func createStmtArgs(tokenslice [][]string, f *ConsumeFuzzer) (string, error) { |
| var query, token strings.Builder |
| |
| // We go through the tokens in the tokenslice, |
| // create the respective token and add it to |
| // "query" |
| for _, tokens := range tokenslice { |
| // For extra randomization, the fuzzer can |
| // choose to not include this token. |
| includeThisToken, err := f.GetBool() |
| if err != nil { |
| return "", err |
| } |
| if !includeThisToken { |
| continue |
| } |
| |
| // There may be several tokens to choose from: |
| if len(tokens) > 1 { |
| chosenToken, err := chooseToken(tokens, f) |
| if err != nil { |
| return "", err |
| } |
| query.WriteString(" " + chosenToken) |
| } else { |
| token.WriteString(tokens[0]) |
| |
| // In case the token is "CUSTOM_FUZZ_STRING" |
| // we will then create a non-structured string |
| if token.String() == "CUSTOM_FUZZ_STRING" { |
| customFuzzString, err := f.GetString() |
| if err != nil { |
| return "", err |
| } |
| query.WriteString(" " + customFuzzString) |
| continue |
| } |
| |
| // Check if token requires an argument. |
| // Tokens that take an argument can be found |
| // in 'needCustomString'. If so, we add a |
| // non-structured string to the token. |
| if containsString(needCustomString, token.String()) { |
| customFuzzString, err := f.GetString() |
| if err != nil { |
| return "", err |
| } |
| token.WriteString(fmt.Sprintf(" %s", customFuzzString)) |
| } |
| query.WriteString(fmt.Sprintf(" %s", token.String())) |
| } |
| } |
| return query.String(), nil |
| } |
| |
| // Creates a semi-structured query. It creates a string |
| // that is a combination of the keywords and random strings. |
| func createQuery(f *ConsumeFuzzer) (string, error) { |
| queryLen, err := f.GetInt() |
| if err != nil { |
| return "", err |
| } |
| maxLen := queryLen % 60 |
| if maxLen == 0 { |
| return "", fmt.Errorf("could not create a query") |
| } |
| var query strings.Builder |
| for i := 0; i < maxLen; i++ { |
| // Get a new token: |
| useKeyword, err := f.GetBool() |
| if err != nil { |
| return "", err |
| } |
| if useKeyword { |
| keyword, err := getKeyword(f) |
| if err != nil { |
| return "", err |
| } |
| query.WriteString(" " + keyword) |
| } else { |
| customString, err := f.GetString() |
| if err != nil { |
| return "", err |
| } |
| query.WriteString(" " + customString) |
| } |
| } |
| if query.String() == "" { |
| return "", fmt.Errorf("could not create a query") |
| } |
| return query.String(), nil |
| } |
| |
| // GetSQLString is the API that users interact with. |
| // |
| // Usage: |
| // |
| // f := NewConsumer(data) |
| // sqlString, err := f.GetSQLString() |
| func (f *ConsumeFuzzer) GetSQLString() (string, error) { |
| var query string |
| veryStructured, err := f.GetBool() |
| if err != nil { |
| return "", err |
| } |
| if veryStructured { |
| query, err = createStmt(f) |
| if err != nil { |
| return "", err |
| } |
| } else { |
| query, err = createQuery(f) |
| if err != nil { |
| return "", err |
| } |
| } |
| return query, nil |
| } |