Skip to content

Commit

Permalink
Extend exists operator to support key paths and negation
Browse files Browse the repository at this point in the history
  • Loading branch information
Anilm3 committed Aug 15, 2024
1 parent 506c87d commit ec70644
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 15 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
121 changes: 121 additions & 0 deletions src/condition/exists.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// 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 "condition/exists.hpp"
#include "utils.hpp"

namespace ddwaf {

namespace {

enum class search_outcome { found, not_found, unknown };

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 (objects_excluded.contains(root) || key_path.size() > limits.max_container_depth) {
// The object might be present, but we can't know for sure
return search_outcome::unknown;
}

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;
}

const ddwaf_object *parent = root;
auto it = key_path.begin();

std::size_t depth = 0;
std::size_t size = parent->nbEntries;

for (std::size_t i = 0; i < size;) {
const auto &child = parent->array[i++];

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

if (key == *it) {
if (objects_excluded.contains(&child)) {
// We found the next child 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 (child.type != DDWAF_OBJ_MAP) {
return search_outcome::not_found;
}

if (++depth >= limits.max_container_depth) {
return search_outcome::unknown;
}

// Reset the loop and iterate child
parent = &child;
i = 0;
size = std::min(static_cast<uint32_t>(child.nbEntries), limits.max_container_size);
}
}

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
{
if (inputs.empty()) {
return {false, false};
}
// We only care about the first input
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
20 changes: 16 additions & 4 deletions src/parser/expression_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ std::shared_ptr<expression> parse_expression(const parameter::vector &conditions
auto operator_name = at<std::string_view>(root, "operator");
auto params = at<parameter::map>(root, "parameters");

bool negated = operator_name.starts_with("!");
if (negated) {
operator_name = operator_name.substr(1);
}

if (operator_name == "lfi_detector") {
auto arguments =
parse_arguments<lfi_detector>(params, source, transformers, addresses, limits);
Expand All @@ -116,10 +121,17 @@ std::shared_ptr<expression> parse_expression(const parameter::vector &conditions
parse_arguments<shi_detector>(params, source, transformers, addresses, limits);
conditions.emplace_back(std::make_unique<shi_detector>(std::move(arguments), limits));
} else if (operator_name == "exists") {
auto arguments =
parse_arguments<exists_condition>(params, source, transformers, addresses, limits);
conditions.emplace_back(
std::make_unique<exists_condition>(std::move(arguments), limits));
if (negated) {
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 arguments = parse_arguments<exists_condition>(
params, source, transformers, addresses, limits);
conditions.emplace_back(
std::make_unique<exists_condition>(std::move(arguments), limits));
}
} else {
auto [data_id, matcher] = parse_matcher(operator_name, params);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,38 @@ TEST(TestExistsCondition, AddressAvailable)
ASSERT_TRUE(res.outcome);
}

TEST(TestExistsCondition, KeyPathAvailable)
{
exists_condition cond{{{{{{"server.request.uri_raw", get_target_index("server.request.uri_raw"),
{"path", "to", "object"}}}}}}};

ddwaf_object tmp;
ddwaf_object path;
ddwaf_object to;
ddwaf_object object;

ddwaf_object_map(&object);
ddwaf_object_map_add(&object, "object", ddwaf_object_invalid(&tmp));

ddwaf_object_map(&to);
ddwaf_object_map_add(&to, "to", &object);

ddwaf_object_map(&path);
ddwaf_object_map_add(&path, "path", &to);

ddwaf_object root;
ddwaf_object_map(&root);
ddwaf_object_map_add(&root, "server.request.uri_raw", &path);

object_store store;
store.insert(root);

ddwaf::timer deadline{2s};
condition_cache cache;
auto res = cond.eval(cache, store, {}, {}, deadline);
ASSERT_TRUE(res.outcome);
}

TEST(TestExistsCondition, AddressNotAvaialble)
{
exists_condition cond{{gen_variadic_param("server.request.uri_raw")}};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
name: "Basic run with exists operator with key_path",
runs: [
{
persistent-input: {
rule2-input: {
"path": {
"to": {
"object": "something else"
}
}
}
},
rules: [
{
2: [
{
address: rule2-input,
key_path: ["path", "to", "object"]
}
]
}
],
code: match
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
name: "Basic run with exists operator with key_path and no match",
runs: [
{
persistent-input: {
rule2-input: {
"path": {
"to": {
"nowhere": "something else"
}
}
}
},
code: ok
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
name: "Basic run with !exists operator with key_path",
runs: [
{
persistent-input: {
rule3-input: {
"path": {
"to": {
"nowhere": "something else"
}
}
}
},
rules: [
{
3: [
{
address: rule3-input,
key_path: ["path", "to", "object"]
}
]
}
],
code: match
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
name: "Basic run with !exists operator with key_path and no match",
runs: [
{
persistent-input: {
rule3-input: {
"path": {
"to": {
"object": "something else"
}
}
}
},
code: ok
}
]
}
22 changes: 22 additions & 0 deletions validator/tests/rules/operators/exists/ruleset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,25 @@ rules:
parameters:
inputs:
- address: rule1-input
- id: "2"
name: rule2-exists-keypath
tags:
type: flow2
category: category
conditions:
- operator: exists
parameters:
inputs:
- address: rule2-input
key_path: ["path", "to", "object"]
- id: "3"
name: rule3-does-not-exist-keypath
tags:
type: flow3
category: category
conditions:
- operator: "!exists"
parameters:
inputs:
- address: rule3-input
key_path: ["path", "to", "object"]

0 comments on commit ec70644

Please sign in to comment.