Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend exists operator to support key paths and negation #334

Merged
merged 11 commits into from
Sep 18, 2024
Merged
1 change: 1 addition & 0 deletions cmake/objects.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ set(LIBDDWAF_SOURCE
${libddwaf_SOURCE_DIR}/src/parser/exclusion_parser.cpp
${libddwaf_SOURCE_DIR}/src/processor/extract_schema.cpp
${libddwaf_SOURCE_DIR}/src/processor/fingerprint.cpp
${libddwaf_SOURCE_DIR}/src/condition/exists.cpp
${libddwaf_SOURCE_DIR}/src/condition/lfi_detector.cpp
${libddwaf_SOURCE_DIR}/src/condition/sqli_detector.cpp
${libddwaf_SOURCE_DIR}/src/condition/ssrf_detector.cpp
Expand Down
127 changes: 127 additions & 0 deletions src/condition/exists.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Unless explicitly stated otherwise all files in this repository are
// dual-licensed under the Apache-2.0 License or BSD-3-Clause License.
//
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2021 Datadog, Inc.
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "argument_retriever.hpp"
#include "clock.hpp"
#include "condition/base.hpp"
#include "condition/exists.hpp"
#include "ddwaf.h"
#include "exception.hpp"
#include "exclusion/common.hpp"
#include "utils.hpp"

namespace ddwaf {

namespace {

enum class search_outcome { found, not_found, unknown };

const ddwaf_object *find_key(
const ddwaf_object &parent, std::string_view key, const object_limits &limits)
{
const std::size_t size =
std::min(static_cast<uint32_t>(parent.nbEntries), limits.max_container_size);
for (std::size_t i = 0; i < size; ++i) {
const auto &child = parent.array[i];

if (child.parameterName == nullptr) [[unlikely]] {
continue;
}
const std::string_view child_key{
child.parameterName, static_cast<std::size_t>(child.parameterNameLength)};

if (key == child_key) {
return &child;
}
}

return nullptr;
}

search_outcome exists(const ddwaf_object *root, std::span<const std::string> key_path,
const exclusion::object_set_ref &objects_excluded, const object_limits &limits)
{
if (key_path.empty()) {
return search_outcome::found;
}

// Since there's a key path, the object must be a map
if (root->type != DDWAF_OBJ_MAP) {
return search_outcome::not_found;
Anilm3 marked this conversation as resolved.
Show resolved Hide resolved
}

auto it = key_path.begin();

// The parser ensures that the key path is within the limits specified by
// the user, hence we don't need to check for depth
while ((root = find_key(*root, *it, limits)) != nullptr) {
if (objects_excluded.contains(root)) {
// We found the next root but it has been excluded, so we
// can't know for sure if the required key path exists
return search_outcome::unknown;
}

if (++it == key_path.end()) {
return search_outcome::found;
}

if (root->type != DDWAF_OBJ_MAP) {
return search_outcome::not_found;
}
}

return search_outcome::not_found;
}

} // namespace

[[nodiscard]] eval_result exists_condition::eval_impl(
const variadic_argument<const ddwaf_object *> &inputs, condition_cache &cache,
const exclusion::object_set_ref &objects_excluded, ddwaf::timer &deadline) const
{
for (const auto &input : inputs) {
if (deadline.expired()) {
throw ddwaf::timeout_exception();
}

if (exists(input.value, input.key_path, objects_excluded, limits_) ==
search_outcome::found) {
std::vector<std::string> key_path{input.key_path.begin(), input.key_path.end()};
cache.match = {{{{"input", {}, input.address, std::move(key_path)}}, {}, "exists", {},
input.ephemeral}};
return {true, input.ephemeral};
}
}
return {false, false};
}

[[nodiscard]] eval_result exists_negated_condition::eval_impl(
const unary_argument<const ddwaf_object *> &input, condition_cache &cache,
const exclusion::object_set_ref &objects_excluded, ddwaf::timer & /*deadline*/) const
{
// We need to make sure the key path hasn't been found. If the result is
// unknown, we can't guarantee that the key path isn't actually present in
// the data set
if (exists(input.value, input.key_path, objects_excluded, limits_) !=
search_outcome::not_found) {
return {false, false};
}

std::vector<std::string> key_path{input.key_path.begin(), input.key_path.end()};
cache.match = {
{{{"input", {}, input.address, std::move(key_path)}}, {}, "!exists", {}, input.ephemeral}};
return {true, input.ephemeral};
}

} // namespace ddwaf
32 changes: 21 additions & 11 deletions src/condition/exists.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#pragma once

#include "condition/structured_condition.hpp"
#include "exception.hpp"
#include "iterator.hpp"

namespace ddwaf {

Expand All @@ -21,19 +23,27 @@ class exists_condition : public base_impl<exists_condition> {

protected:
[[nodiscard]] eval_result eval_impl(const variadic_argument<const ddwaf_object *> &inputs,
condition_cache &cache, const exclusion::object_set_ref & /*objects_excluded*/,
ddwaf::timer & /*deadline*/) const
{
if (inputs.empty()) {
return {false, false};
}
// We only care about the first input
auto input = inputs.front();
cache.match = {{{{"input", {}, input.address, {}}}, {}, "exists", {}, input.ephemeral}};
return {true, input.ephemeral};
}
condition_cache &cache, const exclusion::object_set_ref &objects_excluded,
ddwaf::timer &deadline) const;

friend class base_impl<exists_condition>;
};

class exists_negated_condition : public base_impl<exists_negated_condition> {
public:
static constexpr std::array<std::string_view, 1> param_names{"inputs"};

explicit exists_negated_condition(
std::vector<condition_parameter> args, const object_limits &limits = {})
: base_impl<exists_negated_condition>(std::move(args), limits)
{}

protected:
[[nodiscard]] eval_result eval_impl(const unary_argument<const ddwaf_object *> &input,
condition_cache &cache, const exclusion::object_set_ref &objects_excluded,
ddwaf::timer & /*deadline*/) const;

friend class base_impl<exists_negated_condition>;
};

} // namespace ddwaf
9 changes: 9 additions & 0 deletions src/parser/expression_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ std::vector<condition_parameter> parse_arguments(const parameter::map &params, d
}

auto kp = at<std::vector<std::string>>(input, "key_path", {});
if (kp.size() > limits.max_container_depth) {
throw ddwaf::parsing_error("key_path beyond maximum container depth");
}

for (const auto &path : kp) {
if (path.empty()) {
throw ddwaf::parsing_error("empty key_path");
Expand Down Expand Up @@ -131,6 +135,11 @@ std::shared_ptr<expression> parse_expression(const parameter::vector &conditions
parse_arguments<exists_condition>(params, source, transformers, addresses, limits);
conditions.emplace_back(
std::make_unique<exists_condition>(std::move(arguments), limits));
} else if (operator_name == "!exists") {
auto arguments = parse_arguments<exists_negated_condition>(
params, source, transformers, addresses, limits);
conditions.emplace_back(
std::make_unique<exists_negated_condition>(std::move(arguments), limits));
} else {
auto [data_id, matcher] = parse_matcher(operator_name, params);

Expand Down
Loading
Loading