Skip to content

Commit

Permalink
Session Fingerprint (#322)
Browse files Browse the repository at this point in the history
  • Loading branch information
Anilm3 authored Jul 17, 2024
1 parent 09cc544 commit bfd0a5b
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 22 deletions.
10 changes: 10 additions & 0 deletions src/builder/processor_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ template <> struct typed_processor_builder<http_network_fingerprint> {
}
};

template <> struct typed_processor_builder<session_fingerprint> {
std::shared_ptr<base_processor> build(const auto &spec)
{
return std::make_shared<session_fingerprint>(
spec.id, spec.expr, spec.mappings, spec.evaluate, spec.output);
}
};

template <typename T, typename Spec, typename Scanners>
concept has_build_with_scanners =
requires(typed_processor_builder<T> b, Spec spec, Scanners scanners) {
Expand Down Expand Up @@ -101,6 +109,8 @@ template <typename T>
return build_with_type<http_header_fingerprint>(*this, scanners);
case processor_type::http_network_fingerprint:
return build_with_type<http_network_fingerprint>(*this, scanners);
case processor_type::session_fingerprint:
return build_with_type<session_fingerprint>(*this, scanners);
default:
break;
}
Expand Down
7 changes: 4 additions & 3 deletions src/parser/processor_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ processor_container parse_processors(
type = processor_type::http_header_fingerprint;
} else if (generator_id == "session_fingerprint") {
type = processor_type::session_fingerprint;
// Skip for now
continue;
} else {
DDWAF_WARN("Unknown generator: {}", generator_id);
info.add_failed(id, "unknown generator '" + generator_id + "'");
Expand All @@ -106,9 +104,12 @@ processor_container parse_processors(
} else if (type == processor_type::http_header_fingerprint) {
mappings = parse_processor_mappings(
mappings_vec, addresses, http_header_fingerprint::param_names);
} else {
} else if (type == processor_type::http_network_fingerprint) {
mappings = parse_processor_mappings(
mappings_vec, addresses, http_network_fingerprint::param_names);
} else {
mappings = parse_processor_mappings(
mappings_vec, addresses, session_fingerprint::param_names);
}

std::vector<reference_spec> scanners;
Expand Down
167 changes: 151 additions & 16 deletions src/processor/fingerprint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@ struct key_hash_field : field_generator {
key_hash_field &operator=(const key_hash_field &) = default;
key_hash_field &operator=(key_hash_field &&) = default;

std::size_t length() override { return value.type == DDWAF_OBJ_MAP ? 8 : 0; }
std::size_t length() override
{
return value.type == DDWAF_OBJ_MAP && value.nbEntries > 0 ? 8 : 0;
}
void operator()(string_buffer &output) override;

ddwaf_object value;
Expand All @@ -188,6 +191,23 @@ struct vector_hash_field : field_generator {
const std::vector<std::string> &value;
};

struct kv_hash_fields : field_generator {
explicit kv_hash_fields(const ddwaf_object &input) : value(input) {}
~kv_hash_fields() override = default;
kv_hash_fields(const kv_hash_fields &) = default;
kv_hash_fields(kv_hash_fields &&) = default;
kv_hash_fields &operator=(const kv_hash_fields &) = default;
kv_hash_fields &operator=(kv_hash_fields &&) = default;

std::size_t length() override
{
return value.type == DDWAF_OBJ_MAP && value.nbEntries > 0 ? (8 + 1 + 8) : 1;
}
void operator()(string_buffer &output) override;

ddwaf_object value;
};

template <typename... Generators> std::size_t generate_fragment_length(Generators &...generators)
{
static_assert(sizeof...(generators) > 0, "At least one generator is required");
Expand Down Expand Up @@ -233,10 +253,14 @@ bool str_casei_cmp(std::string_view left, std::string_view right)
return lc < rc;
}
}
return left.size() <= right.size();
return left.size() < right.size();
}

void normalize_string(std::string_view key, std::string &buffer, bool trailing_separator)
// Default key normalization implies:
// - Lowercasing the string
// - Escaping commas
// - Adding trailing commas
void normalize_key(std::string_view key, std::string &buffer, bool trailing_separator)
{
buffer.clear();

Expand All @@ -258,6 +282,48 @@ void normalize_string(std::string_view key, std::string &buffer, bool trailing_s
}
}

// Header normalization implies:
// - Lowercasing the header
// - Replacing '_' with '-'
void normalize_header(std::string_view original, std::string &buffer)
{
buffer.resize(original.size());

for (std::size_t i = 0; i < original.size(); ++i) {
const auto c = original[i];
buffer[i] = c == '_' ? '-' : ddwaf::tolower(c);
}
}

// Value (as opposed to key) normalisation only requires escaping commas
void normalize_value(std::string_view key, std::string &buffer, bool trailing_separator)
{
buffer.clear();

if (buffer.capacity() < key.size()) {
// Add space for the extra comma, just in case
buffer.reserve(key.size() + 1);
}

for (std::size_t i = 0; i < key.size(); ++i) {
auto comma_idx = key.find(',', i);
if (comma_idx != std::string_view::npos) {
if (comma_idx != i) {
buffer.append(key.substr(i, comma_idx - i));
}
buffer.append(R"(\,)");
i = comma_idx;
} else {
buffer.append(key.substr(i));
break;
}
}

if (trailing_separator) {
buffer.append(1, ',');
}
}

void string_hash_field::operator()(string_buffer &output)
{
if (value.empty()) {
Expand All @@ -275,18 +341,22 @@ void string_hash_field::operator()(string_buffer &output)

void key_hash_field::operator()(string_buffer &output)
{
if (value.type != DDWAF_OBJ_MAP or value.nbEntries == 0) {
if (value.type != DDWAF_OBJ_MAP || value.nbEntries == 0) {
return;
}

std::vector<std::string_view> keys;
keys.reserve(value.nbEntries);

std::size_t max_string_size = 0;
for (unsigned i = 0; i < value.nbEntries; ++i) {
const auto &child = value.array[i];

std::string_view key{
child.parameterName, static_cast<std::size_t>(child.parameterNameLength)};
if (max_string_size > key.size()) {
max_string_size = key.size();
}

keys.emplace_back(key);
}
Expand All @@ -295,8 +365,12 @@ void key_hash_field::operator()(string_buffer &output)

sha256_hash hasher;
std::string normalized;
// By reserving the largest possible size, it should reduce reallocations
// We also add +1 to account for the trailing comma
normalized.reserve(max_string_size + 1);
for (unsigned i = 0; i < keys.size(); ++i) {
normalize_string(keys[i], normalized, (i + 1) < keys.size());
bool trailing_comma = ((i + 1) < keys.size());
normalize_key(keys[i], normalized, trailing_comma);
hasher << normalized;
}

Expand All @@ -319,6 +393,63 @@ void vector_hash_field::operator()(string_buffer &output)
hasher.write_digest(output.subspan<8>());
}

void kv_hash_fields::operator()(string_buffer &output)
{
if (value.nbEntries == 0) {
output.append('-');
return;
}

std::vector<std::pair<std::string_view, std::string_view>> kv_sorted;
kv_sorted.reserve(value.nbEntries);

std::size_t max_string_size = 0;
for (std::size_t i = 0; i < value.nbEntries; ++i) {
const auto &child = value.array[i];

std::string_view key{
child.parameterName, static_cast<std::size_t>(child.parameterNameLength)};

std::string_view val;
if (child.type == DDWAF_OBJ_STRING) {
val = std::string_view{child.stringValue, static_cast<std::size_t>(child.nbEntries)};
}

auto larger_size = std::max(key.size(), val.size());
if (max_string_size < larger_size) {
max_string_size = larger_size;
}

kv_sorted.emplace_back(key, val);
}

std::sort(kv_sorted.begin(), kv_sorted.end(),
[](auto &left, auto &right) { return str_casei_cmp(left.first, right.first); });

sha256_hash key_hasher;
sha256_hash val_hasher;

std::string normalized;
// By reserving the largest possible size, it should reduce reallocations
// We also add +1 to account for the trailing comma
normalized.reserve(max_string_size + 1);
for (unsigned i = 0; i < kv_sorted.size(); ++i) {
auto [key, val] = kv_sorted[i];

bool trailing_comma = ((i + 1) < kv_sorted.size());

normalize_key(key, normalized, trailing_comma);
key_hasher << normalized;

normalize_value(val, normalized, trailing_comma);
val_hasher << normalized;
}

key_hasher.write_digest(output.subspan<8>());
output.append('-');
val_hasher.write_digest(output.subspan<8>());
}

enum class header_type { unknown, standard, ip_origin, user_agent, datadog };

constexpr std::size_t standard_headers_length = 10;
Expand Down Expand Up @@ -355,17 +486,6 @@ std::pair<header_type, unsigned> get_header_type_and_index(std::string_view head
return it->second;
}

// "normalized" is a preallocated std::string, to avoid unnecessary allocations
void normalize_header(std::string_view original, std::string &normalized)
{
normalized.resize(original.size());

for (std::size_t i = 0; i < original.size(); ++i) {
const auto c = original[i];
normalized[i] = c == '_' ? '-' : ddwaf::tolower(c);
}
}

} // namespace

// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
Expand Down Expand Up @@ -482,4 +602,19 @@ std::pair<ddwaf_object, object_store::attribute> http_network_fingerprint::eval_
return {res, object_store::attribute::none};
}

// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
std::pair<ddwaf_object, object_store::attribute> session_fingerprint::eval_impl(
const unary_argument<const ddwaf_object *> &cookies,
const unary_argument<std::string_view> &session_id,
const unary_argument<std::string_view> &user_id, ddwaf::timer &deadline) const
{
if (deadline.expired()) {
throw ddwaf::timeout_exception();
}

auto res = generate_fragment("ssn", string_hash_field{user_id.value},
kv_hash_fields{*cookies.value}, string_hash_field{session_id.value});
return {res, object_store::attribute::none};
}

} // namespace ddwaf
3 changes: 0 additions & 3 deletions src/processor/fingerprint.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@

#include <cstdlib>
#include <cstring>
#include <span>
#include <string_view>

#include "processor/base.hpp"
#include "scanner.hpp"
#include "utils.hpp"

namespace ddwaf {

Expand Down
Loading

0 comments on commit bfd0a5b

Please sign in to comment.