#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
#include "argument_retriever.hpp"
#include "clock.hpp"
#include "condition/base.hpp"
#include "condition/match_iterator.hpp"
#include "condition/sqli_detector.hpp"
#include "condition/structured_condition.hpp"
#include "exception.hpp"
#include "exclusion/common.hpp"
#include "log.hpp"
#include "object.hpp"
#include "tokenizer/generic_sql.hpp"
#include "tokenizer/mysql.hpp"
#include "tokenizer/pgsql.hpp"
#include "tokenizer/sql_base.hpp"
#include "tokenizer/sqlite.hpp"
#include "transformer/base.hpp"
#include "utils.hpp"
using namespace std::literals;
namespace ddwaf {
namespace internal {
namespace {
constexpr auto &npos = std::string_view::npos;
constexpr std::size_t min_token_count = 4;
enum class sqli_error : uint8_t {
none,
invalid_sql,
};
using matched_param = std::pair<std::string, std::vector<std::variant<std::string_view, int64_t>>>;
using sqli_result = std::variant<std::monostate, sqli_error, matched_param>;
bool is_literal(sql_token_type type)
{
return type == sql_token_type::number || type == sql_token_type::double_quoted_string ||
type == sql_token_type::single_quoted_string ||
type == sql_token_type::dollar_quoted_string ||
type == sql_token_type::back_quoted_string;
}
bool is_asc_or_desc(std::string_view str)
{
return string_iequals_literal(str, "ASC") || string_iequals_literal(str, "DESC");
}
bool is_limit_or_offset(std::string_view str)
{
return string_iequals_literal(str, "LIMIT") || string_iequals_literal(str, "OFFSET");
}
}
std::string strip_literals(std::string_view statement, std::span<sql_token> tokens)
{
std::string stripped;
stripped.reserve(statement.size());
std::size_t i = 0;
for (auto token : tokens) {
if (!is_literal(token.type)) {
continue;
}
if (i < token.index) {
stripped.append(statement.substr(i, token.index - i));
i = token.index;
}
stripped.append(1, '?');
i += token.str.size();
}
if (i < statement.size()) {
stripped.append(statement.substr(i));
}
return stripped;
}
bool is_query_comment(std::string_view resource, std::span<sql_token> param_tokens,
std::size_t param_end )
{
if (param_tokens.size() == 1) {
return false;
}
for (std::size_t i = 0; i < param_tokens.size(); ++i) {
const auto &token = param_tokens[i];
if (token.type == sql_token_type::eol_comment) {
if (i == (param_tokens.size() - 1) && token.str.starts_with("--")) {
auto injection = resource.substr(token.index, param_end - token.index);
if (!injection.starts_with("--")) {
return false;
}
}
return i > 0;
}
}
return false;
}
bool is_where_tautology(const std::vector<sql_token> &resource_tokens,
std::span<sql_token> param_tokens,
std::size_t param_tokens_begin )
{
if (param_tokens.size() != 3 || param_tokens_begin == 0) {
return false;
}
bool where_found = false;
for (std::size_t i = 0; i < param_tokens_begin; ++i) {
const auto &token = resource_tokens[i];
if (token.type == sql_token_type::keyword && string_iequals_literal(token.str, "where")) {
where_found = true;
break;
}
}
if (!where_found) {
return false;
}
auto middle_token = param_tokens[1];
if (middle_token.type != sql_token_type::binary_operator) {
return string_iequals_literal(middle_token.str, "OR") ||
string_iequals_literal(middle_token.str, "XOR") || middle_token.str == "||";
}
if (param_tokens[0].type != param_tokens[2].type && middle_token.str.find('=') != npos) {
return param_tokens[0].type != sql_token_type::keyword &&
param_tokens[0].type != sql_token_type::identifier &&
param_tokens[2].type != sql_token_type::identifier &&
param_tokens[2].type != sql_token_type::keyword;
}
return true;
}
bool has_order_by_structure(std::span<sql_token> tokens)
{
enum class order_by_state : uint8_t {
invalid,
begin,
table_or_column_name,
column_name,
column_index,
limit_or_offset_value,
limit_or_offset,
asc_or_desc,
comma,
dot,
end,
};
auto current_state = order_by_state::begin;
auto current_token = tokens.begin();
while (current_state != order_by_state::end) {
auto next_state = order_by_state::invalid;
switch (current_state) {
case order_by_state::begin:
case order_by_state::comma:
if (current_token == tokens.end()) {
break;
}
if (current_token->type == sql_token_type::number) {
next_state = order_by_state::column_index;
} else if (current_token->type == sql_token_type::identifier) {
next_state = order_by_state::table_or_column_name;
}
break;
case order_by_state::table_or_column_name:
if (current_token == tokens.end()) {
next_state = order_by_state::end;
break;
}
if (current_token->type == sql_token_type::dot) {
next_state = order_by_state::dot;
} else if (current_token->type == sql_token_type::comma) {
next_state = order_by_state::comma;
} else if (current_token->type == sql_token_type::keyword) {
if (is_asc_or_desc(current_token->str)) {
next_state = order_by_state::asc_or_desc;
} else if (is_limit_or_offset(current_token->str)) {
next_state = order_by_state::limit_or_offset;
}
}
break;
case order_by_state::column_index:
case order_by_state::column_name:
if (current_token == tokens.end()) {
next_state = order_by_state::end;
break;
}
if (current_token->type == sql_token_type::comma) {
next_state = order_by_state::comma;
} else if (current_token->type == sql_token_type::keyword) {
if (is_asc_or_desc(current_token->str)) {
next_state = order_by_state::asc_or_desc;
} else if (is_limit_or_offset(current_token->str)) {
next_state = order_by_state::limit_or_offset;
}
}
break;
case order_by_state::limit_or_offset_value:
if (current_token == tokens.end()) {
next_state = order_by_state::end;
break;
}
if (current_token->type == sql_token_type::keyword &&
is_limit_or_offset(current_token->str)) {
next_state = order_by_state::limit_or_offset;
}
break;
case order_by_state::limit_or_offset:
if (current_token == tokens.end()) {
break;
}
if (current_token->type == sql_token_type::number) {
next_state = order_by_state::limit_or_offset_value;
}
break;
case order_by_state::asc_or_desc:
if (current_token == tokens.end()) {
next_state = order_by_state::end;
break;
}
if (current_token->type == sql_token_type::comma) {
next_state = order_by_state::comma;
} else if (current_token->type == sql_token_type::keyword &&
is_limit_or_offset(current_token->str)) {
next_state = order_by_state::limit_or_offset;
}
break;
case order_by_state::dot:
if (current_token == tokens.end()) {
break;
}
if (current_token->type == sql_token_type::identifier) {
next_state = order_by_state::column_name;
}
break;
default:
return false;
}
if (next_state == order_by_state::invalid) {
return false;
}
current_state = next_state;
current_token++;
}
return true;
}
bool is_benign_order_by_clause(const std::vector<sql_token> &resource_tokens,
std::span<sql_token> param_tokens, std::size_t param_tokens_begin)
{
if (param_tokens_begin < 2) {
return false;
}
const std::string_view order = resource_tokens[param_tokens_begin - 2].str;
const std::string_view by = resource_tokens[param_tokens_begin - 1].str;
if (!string_iequals_literal(order, "order") || !string_iequals_literal(by, "by")) {
return false;
}
return has_order_by_structure(param_tokens);
}
bool contains_harmful_tokens(std::span<sql_token> tokens)
{
if (tokens.size() < min_token_count) {
return false;
}
for (const auto &token : tokens) {
auto t = token.type;
if (t != sql_token_type::comma && t != sql_token_type::parenthesis_close &&
t != sql_token_type::parenthesis_open && t != sql_token_type::number &&
t != sql_token_type::single_quoted_string &&
t != sql_token_type::double_quoted_string &&
t != sql_token_type::dollar_quoted_string) {
return true;
}
}
return false;
}
std::pair<std::span<sql_token>, std::size_t> get_consecutive_tokens(
std::vector<sql_token> &resource_tokens, std::size_t begin, std::size_t end)
{
std::size_t index_begin = std::numeric_limits<std::size_t>::max();
std::size_t index_end = 0;
for (std::size_t i = 0; i < resource_tokens.size(); ++i) {
const auto &rtoken = resource_tokens[i];
auto rtoken_end = rtoken.index + rtoken.str.size();
if (rtoken_end > begin) {
if (rtoken.index < end) {
index_begin = std::min(i, index_begin);
if (i > index_end || i == (resource_tokens.size() - 1)) {
index_end = i;
}
} else {
break;
}
}
}
if (index_begin > index_end) {
return {};
}
return {std::span<sql_token>{&resource_tokens[index_begin], index_end - index_begin + 1},
index_begin};
}
namespace {
template <typename T> std::vector<sql_token> tokenize_helper(std::string_view statement)
{
T tokenizer(statement, {sql_token_type::parenthesis_open, sql_token_type::parenthesis_close});
return tokenizer.tokenize();
}
std::vector<sql_token> tokenize(std::string_view statement, sql_dialect dialect)
{
try {
switch (dialect) {
case sql_dialect::pgsql:
return tokenize_helper<pgsql_tokenizer>(statement);
case sql_dialect::mysql:
return tokenize_helper<mysql_tokenizer>(statement);
case sql_dialect::sqlite:
return tokenize_helper<sqlite_tokenizer>(statement);
default:
break;
}
return tokenize_helper<generic_sql_tokenizer>(statement);
} catch (const std::runtime_error &e) {
DDWAF_DEBUG("Failed to load tokenizer for dialect: {}", dialect);
}
return {};
}
sqli_result sqli_impl(std::string_view resource, std::vector<sql_token> &resource_tokens,
object_view params, std::span<const transformer_id> transformers, sql_dialect dialect,
const object_set_ref &objects_excluded, ddwaf::timer &deadline)
{
static constexpr std::size_t min_str_len = 3;
match_iterator<min_str_len> it(resource, params, transformers, objects_excluded, deadline);
for (; it; ++it) {
if (deadline.expired()) {
throw ddwaf::timeout_exception();
}
const auto [value, param_index] = *it;
if (resource_tokens.empty()) {
resource_tokens = tokenize(resource, dialect);
if (resource_tokens.empty()) {
return sqli_error::invalid_sql;
}
}
auto [param_tokens, param_tokens_begin] =
get_consecutive_tokens(resource_tokens, param_index, param_index + value.size());
if (param_tokens.empty()) {
continue;
}
if ((contains_harmful_tokens(param_tokens) &&
!is_benign_order_by_clause(resource_tokens, param_tokens, param_tokens_begin)) ||
(param_tokens.size() < min_token_count &&
(is_where_tautology(resource_tokens, param_tokens, param_tokens_begin) ||
is_query_comment(resource, param_tokens, param_index + value.size())))) {
return matched_param{std::string(value), it.get_current_path()};
}
}
return {};
}
}
}
sqli_detector::sqli_detector(std::vector<condition_parameter> args)
: base_impl<sqli_detector>(std::move(args))
{
generic_sql_tokenizer::initialise_regexes();
mysql_tokenizer::initialise_regexes();
pgsql_tokenizer::initialise_regexes();
sqlite_tokenizer::initialise_regexes();
}
[[nodiscard]] bool sqli_detector::eval_impl(const unary_argument<std::string_view> &sql,
const variadic_argument<object_view> ¶ms, const unary_argument<std::string_view> &db_type,
condition_cache &cache, const object_set_ref &objects_excluded, ddwaf::timer &deadline) const
{
auto dialect = sql_dialect_from_type(db_type.value);
std::vector<sql_token> resource_tokens;
for (const auto ¶m : params) {
auto res = internal::sqli_impl(sql.value, resource_tokens, param.value, param.transformers,
dialect, objects_excluded, deadline);
if (std::holds_alternative<internal::matched_param>(res)) {
auto stripped_stmt = internal::strip_literals(sql.value, resource_tokens);
auto &[highlight, param_kp] = std::get<internal::matched_param>(res);
DDWAF_TRACE("Target {} matched parameter value {}", param.address, highlight);
cache.match = condition_match{.args = {{.name = "resource"sv,
.resolved = stripped_stmt,
.address = sql.address,
.key_path = convert_key_path(sql.key_path)},
{.name = "params"sv,
.resolved = highlight,
.address = param.address,
.key_path = param_kp},
{.name = "db_type"sv,
.resolved = std::string{db_type.value},
.address = db_type.address,
.key_path = {}}},
.highlights = {std::move(highlight)},
.operator_name = "sqli_detector",
.operator_value = {}};
return true;
}
if (std::holds_alternative<internal::sqli_error>(res)) {
break;
}
}
return {};
}
}