Skip to content

Commit

Permalink
Extend exists operator to support key paths and negation (#334)
Browse files Browse the repository at this point in the history
  • Loading branch information
Anilm3 authored Sep 18, 2024
1 parent aeca2d0 commit 749b4fd
Show file tree
Hide file tree
Showing 22 changed files with 1,078 additions and 103 deletions.
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;
}

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

0 comments on commit 749b4fd

Please sign in to comment.