#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <unordered_set>
#include <utility>
#include <variant>
#include <vector>
#include "argument_retriever.hpp"
#include "clock.hpp"
#include "condition/base.hpp"
#include "condition/match_iterator.hpp"
#include "condition/ssrf_detector.hpp"
#include "condition/structured_condition.hpp"
#include "exception.hpp"
#include "exclusion/common.hpp"
#include "ip_utils.hpp"
#include "log.hpp"
#include "matcher/ip_match.hpp"
#include "object.hpp"
#include "transformer/base.hpp"
#include "uri_utils.hpp"
#include "utils.hpp"
using namespace std::literals;
namespace ddwaf {
namespace {
constexpr const auto &npos = std::string_view::npos;
using ssrf_result =
std::optional<std::pair<std::string, std::vector<std::variant<std::string_view, int64_t>>>>;
bool detect_parameter_injection(const ssrf_opts &opts, const uri_decomposed &uri,
std::string_view param, std::size_t param_index)
{
const auto param_end = param_index + param.size();
if (opts.path_inspection && !uri.path.empty()) {
const auto path_end = uri.path_index + uri.path.size();
if (uri.path.size() == param.size()) {
return false;
}
if (uri.path[0] == '/' && param_index == uri.path_index + 1 &&
param.size() == uri.path.size() - 1) {
return false;
}
if (param_index < path_end && param_end > uri.path_index &&
(param.find("%2f") != npos || param.find("%2F") != npos || param.find("%5c") != npos ||
param.find("%5C") != npos)) {
return true;
}
if (auto slash_index = param.find('/'); slash_index != npos) {
slash_index += param_index;
if (slash_index < path_end) {
auto relative_dir_index = param.find("..");
while (relative_dir_index != npos) {
auto dir_index = relative_dir_index + param_index;
if (dir_index < path_end && (dir_index + 2) < uri.raw.size() &&
uri.raw[dir_index - 1] == '/' && uri.raw[dir_index + 2] == '/') {
return true;
}
relative_dir_index = param.find("..", relative_dir_index + 2);
}
}
}
if (param_index < path_end && param_end == uri.raw.size()) {
return false;
}
}
if (opts.query_inspection && !uri.query.empty()) {
const auto param_query_index = param.find('?');
if (uri.query_index != npos && param_query_index != npos &&
(param_index + param_query_index + 1) == uri.query_index) {
auto after_param = uri.raw.substr(param_index + param.length());
return after_param.empty() || after_param[0] != '&';
}
const auto query_end = uri.query_index + uri.query.size();
if ((param_end <= uri.query_index - 1) || param_index >= query_end) {
return false;
}
return param.find('&') != npos;
}
return false;
}
bool is_forbidden_uri(const uri_decomposed &uri, const matcher::ip_match &forbidden_ip_matcher,
const std::unordered_set<std::string_view> &allowed_scheme_set,
const std::vector<std::string> &forbidden_domains)
{
const auto is_forbidden_domain = [&](std::string_view host) {
for (const auto &domain : forbidden_domains) {
if (host.ends_with(domain)) {
return true;
}
}
return false;
};
const auto is_forbidden_ip = [&](const std::optional<ipaddr> &ip) {
return ip.has_value() && forbidden_ip_matcher.match_ip(ip.value());
};
return !allowed_scheme_set.contains(uri.scheme) || is_forbidden_domain(uri.authority.host) ||
is_forbidden_ip(uri.authority.host_ip);
}
ssrf_result ssrf_impl(const uri_decomposed &uri, object_view params,
std::span<const transformer_id> transformers, const object_set_ref &objects_excluded,
const ssrf_opts &opts, const matcher::ip_match &forbidden_ip_matcher,
const std::unordered_set<std::string_view> &allowed_scheme_set,
const std::vector<std::string> &forbidden_domains, ddwaf::timer &deadline)
{
static constexpr std::size_t min_str_len = 4;
std::string_view forbidden_domain = {};
if (!opts.enforce_policy_without_injection) {
for (const auto &domain : forbidden_domains) {
if (uri.authority.host.ends_with(domain)) {
forbidden_domain = domain;
break;
}
}
}
std::optional<ssrf_result> parameter_injection;
match_iterator<min_str_len> it{uri.raw, params, transformers, objects_excluded, deadline};
for (; it; ++it) {
if (deadline.expired()) {
throw ddwaf::timeout_exception();
}
const auto [param, param_index] = *it;
if (uri.raw == param) {
if (opts.forbid_full_url_injection) {
return {{std::string(param), it.get_current_path()}};
}
return {};
}
if (opts.authority_inspection && param_index >= uri.authority.index &&
param_index < uri.scheme_and_authority.size()) {
if ((param_index + param.size() - 1) > uri.scheme_and_authority.size()) {
return {{std::string(param), it.get_current_path()}};
}
const bool host_fully_injected =
param_index <= uri.authority.host_index &&
param_index + param.size() >= uri.authority.host_index + uri.authority.host.size();
if (host_fully_injected && uri.authority.host_ip.has_value() &&
forbidden_ip_matcher.match_ip(uri.authority.host_ip.value())) {
return {{std::string(param), it.get_current_path()}};
}
if (!forbidden_domain.empty() && param.find(forbidden_domain) != npos) {
return {{std::string(param), it.get_current_path()}};
}
}
if (!uri.scheme.empty() && param_index == 0 && param.size() > uri.scheme.size() &&
!allowed_scheme_set.contains(uri.scheme)) {
return {{std::string(param), it.get_current_path()}};
}
if (!parameter_injection.has_value() &&
detect_parameter_injection(opts, uri, param, param_index)) {
parameter_injection = {{std::string(param), it.get_current_path()}};
}
}
if (parameter_injection.has_value()) {
return parameter_injection.value();
}
return {};
}
}
ssrf_detector::ssrf_detector(std::vector<condition_parameter> args)
: base_impl<ssrf_detector>(std::move(args)),
forbidden_ip_matcher_(std::make_unique<matcher::ip_match>(default_forbidden_ips)),
allowed_schemes_(default_allowed_schemes.begin(), default_allowed_schemes.end()),
forbidden_domains_(default_forbidden_domains.begin(), default_forbidden_domains.end())
{}
ssrf_detector::ssrf_detector(std::vector<condition_parameter> args, const ssrf_opts &opts,
std::vector<std::string> &&allowed_schemes, std::vector<std::string> &&forbidden_domains,
const std::vector<std::string_view> &forbidden_ips)
: base_impl<ssrf_detector>(std::move(args)), opts_(opts),
forbidden_ip_matcher_(std::make_unique<matcher::ip_match>(forbidden_ips)),
allowed_schemes_strings_(std::move(allowed_schemes)),
allowed_schemes_(allowed_schemes_strings_.begin(), allowed_schemes_strings_.end()),
forbidden_domains_(std::move(forbidden_domains))
{}
bool ssrf_detector::eval_impl(const unary_argument<std::string_view> &uri,
const variadic_argument<object_view> ¶ms, condition_cache &cache,
const object_set_ref &objects_excluded, ddwaf::timer &deadline) const
{
auto decomposed = uri_parse(uri.value);
if (!decomposed.has_value()) {
return {};
}
if (opts_.enforce_policy_without_injection &&
is_forbidden_uri(
*decomposed, *forbidden_ip_matcher_, allowed_schemes_, forbidden_domains_)) {
DDWAF_TRACE("Resource {} matched policy", uri.value);
cache.match = condition_match{
.args = {{.name = "resource"sv,
.resolved = std::string{uri.value},
.address = uri.address,
.key_path = convert_key_path(uri.key_path)},
{.name = "params"sv, .resolved = {}, .address = {}, .key_path = {}}},
.highlights = {decomposed->scheme_and_authority},
.operator_name = "ssrf_detector",
.operator_value = {}};
return true;
}
for (const auto ¶m : params) {
auto res = ssrf_impl(*decomposed, param.value, param.transformers, objects_excluded, opts_,
*forbidden_ip_matcher_, allowed_schemes_, forbidden_domains_, deadline);
if (res.has_value()) {
auto &[highlight, param_kp] = res.value();
DDWAF_TRACE("Target {} matched parameter value {}", param.address, highlight);
cache.match = condition_match{.args = {{.name = "resource"sv,
.resolved = std::string{uri.value},
.address = uri.address,
.key_path = convert_key_path(uri.key_path)},
{.name = "params"sv,
.resolved = highlight,
.address = param.address,
.key_path = param_kp}},
.highlights = {std::move(highlight)},
.operator_name = "ssrf_detector",
.operator_value = {}};
return true;
}
}
return {};
}
}