Repository: DataDog/libddwaf
Pull request #392: Support single-value arrays when generating fingerprints
Author: login: Anilm3
Created: 2025-03-27T14:23:13Z | Merged: 2025-03-27T18:33:26Z

This PR changes the current fingerprinting processors to tolerate single-element arrays containing a string and treat them as a string. This ensures that the user serialisation of objects can still account for multiple possible values for a given key without the need for a special case.


Diff:
diff --git a/.clang-tidy b/.clang-tidy
index b0153e4fd..6606b9ea0 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -1,7 +1,7 @@
 ---
 # readability-function-cognitive-complexity temporarily disabled until clang-tidy is fixed
 # right now emalloc causes it to misbehave
-Checks:          '*,misc-const-correctness,-bugprone-reserved-identifier,-hicpp-signed-bitwise,-llvmlibc-restrict-system-libc-headers,-altera-unroll-loops,-hicpp-named-parameter,-cert-dcl37-c,-cert-dcl51-cpp,-read,-cppcoreguidelines-init-variables,-cppcoreguidelines-avoid-non-const-global-variables,-altera-id-dependent-backward-branch,-performance-no-int-to-ptr,-altera-struct-pack-align,-google-readability-casting,-modernize-use-trailing-return-type,-llvmlibc-implementation-in-namespace,-llvmlibc-callee-namespace,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-fuchsia-default-arguments-declarations,-fuchsia-overloaded-operator,-cppcoreguidelines-pro-type-union-access,-fuchsia-default-arguments-calls,-cppcoreguidelines-non-private-member-variables-in-classes,-misc-non-private-member-variables-in-classes,-google-readability-todo,-llvm-header-guard,-readability-function-cognitive-complexity,-readability-identifier-length,-cppcoreguidelines-owning-memory,-cert-err58-cpp,-fuchsia-statically-constructed-objects,-google-build-using-namespace,-hicpp-avoid-goto,-cppcoreguidelines-avoid-goto,-hicpp-no-array-decay,-cppcoreguidelines-pro-bounds-array-to-pointer-decay,-cppcoreguidelines-pro-bounds-constant-array-index,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers,-abseil-string-find-str-contains,-bugprone-unchecked-optional-access,-readability-use-anyofallof,-modernize-loop-convert,-cppcoreguidelines-avoid-c-arrays,-hicpp-avoid-c-arrays,-cppcoreguidelines-no-malloc,-llvmlibc-inline-function-decl,-boost-use-ranges,-modernize-use-ranges'
+Checks:          '*,misc-const-correctness,-bugprone-reserved-identifier,-hicpp-signed-bitwise,-llvmlibc-restrict-system-libc-headers,-altera-unroll-loops,-hicpp-named-parameter,-cert-dcl37-c,-cert-dcl51-cpp,-read,-cppcoreguidelines-init-variables,-cppcoreguidelines-avoid-non-const-global-variables,-altera-id-dependent-backward-branch,-performance-no-int-to-ptr,-altera-struct-pack-align,-google-readability-casting,-modernize-use-trailing-return-type,-llvmlibc-implementation-in-namespace,-llvmlibc-callee-namespace,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-fuchsia-default-arguments-declarations,-fuchsia-overloaded-operator,-cppcoreguidelines-pro-type-union-access,-fuchsia-default-arguments-calls,-cppcoreguidelines-non-private-member-variables-in-classes,-misc-non-private-member-variables-in-classes,-google-readability-todo,-llvm-header-guard,-readability-function-cognitive-complexity,-readability-identifier-length,-cppcoreguidelines-owning-memory,-cert-err58-cpp,-fuchsia-statically-constructed-objects,-google-build-using-namespace,-hicpp-avoid-goto,-cppcoreguidelines-avoid-goto,-hicpp-no-array-decay,-cppcoreguidelines-pro-bounds-array-to-pointer-decay,-cppcoreguidelines-pro-bounds-constant-array-index,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers,-abseil-string-find-str-contains,-bugprone-unchecked-optional-access,-readability-use-anyofallof,-modernize-loop-convert,-cppcoreguidelines-avoid-c-arrays,-hicpp-avoid-c-arrays,-cppcoreguidelines-no-malloc,-llvmlibc-inline-function-decl,-boost-use-ranges,-modernize-use-ranges,-readability-redundant-casting'
 WarningsAsErrors: '*'
 HeaderFilterRegex: ''
 CheckOptions:
diff --git a/src/processor/fingerprint.cpp b/src/processor/fingerprint.cpp
index a50a459b0..aaa20a2ad 100644
--- a/src/processor/fingerprint.cpp
+++ b/src/processor/fingerprint.cpp
@@ -166,6 +166,22 @@ void normalize_value(std::string_view key, std::string &buffer, bool trailing_se
     }
 }
 
+std::string_view value_object_to_string(const ddwaf_object &object)
+{
+    if (object.type == DDWAF_OBJ_STRING) {
+        return {object.stringValue, static_cast<std::size_t>(object.nbEntries)};
+    }
+
+    if (object.type == DDWAF_OBJ_ARRAY && object.nbEntries == 1) {
+        const auto &child = object.array[0];
+        if (child.type == DDWAF_OBJ_STRING) {
+            return {child.stringValue, static_cast<std::size_t>(child.nbEntries)};
+        }
+    }
+
+    return {};
+}
+
 template <typename Derived, typename Output = std::string> struct field_generator {
     using output_type = Output;
 
@@ -330,11 +346,7 @@ struct kv_hash_fields : field_generator<kv_hash_fields, std::pair<std::string, s
             const 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 val = value_object_to_string(child);
 
             auto larger_size = std::max(key.size(), val.size());
             max_string_size = std::max(max_string_size, larger_size);
@@ -612,8 +624,8 @@ std::pair<ddwaf_object, object_store::attribute> http_header_fingerprint::eval_i
             known_header_bitset[index] = '1';
         } else if (type == header_type::unknown) {
             unknown_headers.emplace_back(normalized_header);
-        } else if (type == header_type::user_agent && child.type == DDWAF_OBJ_STRING) {
-            user_agent = {child.stringValue, static_cast<std::size_t>(child.nbEntries)};
+        } else if (type == header_type::user_agent) {
+            user_agent = value_object_to_string(child);
         }
     }
     std::sort(unknown_headers.begin(), unknown_headers.end());
@@ -664,10 +676,11 @@ std::pair<ddwaf_object, object_store::attribute> http_network_fingerprint::eval_
             // Verify not only precedence but also type, as a header of an unexpected
             // type will be unlikely to be used unless the framework has somehow
             // broken down the header into constituent IPs
-            if (chosen_header > index && child.type == DDWAF_OBJ_STRING) {
-                chosen_header_value = {
-                    child.stringValue, static_cast<std::size_t>(child.nbEntries)};
-                chosen_header = index;
+            if (chosen_header > index) {
+                chosen_header_value = value_object_to_string(child);
+                if (!chosen_header_value.empty()) {
+                    chosen_header = index;
+                }
             }
         }
     }
diff --git a/tests/unit/processor/fingerprint_test.cpp b/tests/unit/processor/fingerprint_test.cpp
index 43c4d26e6..8f0103ad9 100644
--- a/tests/unit/processor/fingerprint_test.cpp
+++ b/tests/unit/processor/fingerprint_test.cpp
@@ -546,6 +546,121 @@ TEST(TestHttpHeaderFingerprint, UserAgent)
     ddwaf_object_free(&output);
 }
 
+TEST(TestHttpHeaderFingerprint, UserAgentAsArray)
+{
+    ddwaf_object tmp;
+
+    ddwaf_object headers;
+    ddwaf_object_map(&headers);
+    ddwaf_object_map_add(&headers, "referer", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "connection", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "accept-encoding", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "content-encoding", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "cache-control", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "te", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "accept-charset", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "content-type", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "accept", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "accept-language", ddwaf_object_invalid(&tmp));
+
+    ddwaf_object ua_array;
+    ddwaf_object_array(&ua_array);
+    ddwaf_object_array_add(&ua_array, ddwaf_object_string(&tmp, "Random"));
+    ddwaf_object_map_add(&headers, "user-agent", &ua_array);
+
+    http_header_fingerprint gen{"id", {}, {}, false, true};
+
+    ddwaf::timer deadline{2s};
+    processor_cache cache;
+    auto [output, attr] = gen.eval_impl({{}, {}, false, &headers}, cache, deadline);
+    EXPECT_EQ(output.type, DDWAF_OBJ_STRING);
+    EXPECT_EQ(attr, object_store::attribute::none);
+
+    std::string_view output_sv{
+        output.stringValue, static_cast<std::size_t>(static_cast<std::size_t>(output.nbEntries))};
+    EXPECT_STRV(output_sv, "hdr-1111111111-a441b15f-0-");
+
+    ddwaf_object_free(&headers);
+    ddwaf_object_free(&output);
+}
+
+TEST(TestHttpHeaderFingerprint, UserAgentAsArrayInvalidType)
+{
+    ddwaf_object tmp;
+
+    ddwaf_object headers;
+    ddwaf_object_map(&headers);
+    ddwaf_object_map_add(&headers, "referer", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "connection", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "accept-encoding", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "content-encoding", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "cache-control", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "te", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "accept-charset", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "content-type", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "accept", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "accept-language", ddwaf_object_invalid(&tmp));
+
+    ddwaf_object ua_array;
+    ddwaf_object_array(&ua_array);
+    ddwaf_object_array_add(&ua_array, ddwaf_object_unsigned(&tmp, 42));
+    ddwaf_object_map_add(&headers, "user-agent", &ua_array);
+
+    http_header_fingerprint gen{"id", {}, {}, false, true};
+
+    ddwaf::timer deadline{2s};
+    processor_cache cache;
+    auto [output, attr] = gen.eval_impl({{}, {}, false, &headers}, cache, deadline);
+    EXPECT_EQ(output.type, DDWAF_OBJ_STRING);
+    EXPECT_EQ(attr, object_store::attribute::none);
+
+    std::string_view output_sv{
+        output.stringValue, static_cast<std::size_t>(static_cast<std::size_t>(output.nbEntries))};
+    EXPECT_STRV(output_sv, "hdr-1111111111--0-");
+
+    ddwaf_object_free(&headers);
+    ddwaf_object_free(&output);
+}
+
+TEST(TestHttpHeaderFingerprint, MultipleUserAgents)
+{
+    ddwaf_object tmp;
+
+    ddwaf_object headers;
+    ddwaf_object_map(&headers);
+    ddwaf_object_map_add(&headers, "referer", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "connection", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "accept-encoding", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "content-encoding", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "cache-control", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "te", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "accept-charset", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "content-type", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "accept", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "accept-language", ddwaf_object_invalid(&tmp));
+
+    ddwaf_object ua_array;
+    ddwaf_object_array(&ua_array);
+    ddwaf_object_array_add(&ua_array, ddwaf_object_string(&tmp, "Random"));
+    ddwaf_object_array_add(&ua_array, ddwaf_object_string(&tmp, "Bot"));
+    ddwaf_object_map_add(&headers, "user-agent", &ua_array);
+
+    http_header_fingerprint gen{"id", {}, {}, false, true};
+
+    ddwaf::timer deadline{2s};
+    processor_cache cache;
+    auto [output, attr] = gen.eval_impl({{}, {}, false, &headers}, cache, deadline);
+    EXPECT_EQ(output.type, DDWAF_OBJ_STRING);
+    EXPECT_EQ(attr, object_store::attribute::none);
+
+    std::string_view output_sv{
+        output.stringValue, static_cast<std::size_t>(static_cast<std::size_t>(output.nbEntries))};
+    EXPECT_STRV(output_sv, "hdr-1111111111--0-");
+
+    ddwaf_object_free(&headers);
+    ddwaf_object_free(&output);
+}
+
 TEST(TestHttpHeaderFingerprint, ExcludedUnknownHeaders)
 {
     ddwaf_object tmp;
@@ -747,6 +862,118 @@ TEST(TestHttpNetworkFingerprint, AllXFFHeadersMultipleChosenIPs)
     ddwaf_object_free(&output);
 }
 
+TEST(TestHttpNetworkFingerprint, AllXFFHeadersMultipleChosenIPsAsArray)
+{
+    ddwaf_object tmp;
+
+    ddwaf_object headers;
+    ddwaf_object_map(&headers);
+
+    ddwaf_object xff_array;
+    ddwaf_object_array(&xff_array);
+    ddwaf_object_array_add(&xff_array, ddwaf_object_string(&tmp, "192.168.1.1,::1,8.7.6.5"));
+    ddwaf_object_map_add(&headers, "x-forwarded-for", &xff_array);
+    ddwaf_object_map_add(&headers, "x-real-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "true-client-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "x-client-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "x-forwarded", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "forwarded-for", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "x-cluster-client-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "fastly-client-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "cf-connecting-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "cf-connecting-ipv6", ddwaf_object_invalid(&tmp));
+
+    http_network_fingerprint gen{"id", {}, {}, false, true};
+
+    ddwaf::timer deadline{2s};
+    processor_cache cache;
+    auto [output, attr] = gen.eval_impl({{}, {}, false, &headers}, cache, deadline);
+    EXPECT_EQ(output.type, DDWAF_OBJ_STRING);
+    EXPECT_EQ(attr, object_store::attribute::none);
+
+    std::string_view output_sv{
+        output.stringValue, static_cast<std::size_t>(static_cast<std::size_t>(output.nbEntries))};
+    EXPECT_STRV(output_sv, "net-3-1111111111");
+
+    ddwaf_object_free(&headers);
+    ddwaf_object_free(&output);
+}
+
+TEST(TestHttpNetworkFingerprint, AllXFFHeadersMultipleChosenIPsAsArrayInvalidType)
+{
+    ddwaf_object tmp;
+
+    ddwaf_object headers;
+    ddwaf_object_map(&headers);
+
+    ddwaf_object xff_array;
+    ddwaf_object_array(&xff_array);
+    ddwaf_object_array_add(&xff_array, ddwaf_object_unsigned(&tmp, 42));
+    ddwaf_object_map_add(&headers, "x-forwarded-for", &xff_array);
+    ddwaf_object_map_add(&headers, "x-real-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "true-client-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "x-client-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "x-forwarded", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "forwarded-for", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "x-cluster-client-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "fastly-client-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "cf-connecting-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "cf-connecting-ipv6", ddwaf_object_invalid(&tmp));
+
+    http_network_fingerprint gen{"id", {}, {}, false, true};
+
+    ddwaf::timer deadline{2s};
+    processor_cache cache;
+    auto [output, attr] = gen.eval_impl({{}, {}, false, &headers}, cache, deadline);
+    EXPECT_EQ(output.type, DDWAF_OBJ_STRING);
+    EXPECT_EQ(attr, object_store::attribute::none);
+
+    std::string_view output_sv{
+        output.stringValue, static_cast<std::size_t>(static_cast<std::size_t>(output.nbEntries))};
+    EXPECT_STRV(output_sv, "net-0-1111111111");
+
+    ddwaf_object_free(&headers);
+    ddwaf_object_free(&output);
+}
+
+TEST(TestHttpNetworkFingerprint, AllXFFHeadersMultipleChosenIPsDuplicateXFF)
+{
+    ddwaf_object tmp;
+
+    ddwaf_object headers;
+    ddwaf_object_map(&headers);
+
+    ddwaf_object xff_array;
+    ddwaf_object_array(&xff_array);
+    ddwaf_object_array_add(&xff_array, ddwaf_object_string(&tmp, "192.168.1.1,::1,8.7.6.5"));
+    ddwaf_object_array_add(&xff_array, ddwaf_object_string(&tmp, "192.168.1.44"));
+    ddwaf_object_map_add(&headers, "x-forwarded-for", &xff_array);
+    ddwaf_object_map_add(&headers, "x-real-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "true-client-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "x-client-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "x-forwarded", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "forwarded-for", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "x-cluster-client-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "fastly-client-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "cf-connecting-ip", ddwaf_object_invalid(&tmp));
+    ddwaf_object_map_add(&headers, "cf-connecting-ipv6", ddwaf_object_invalid(&tmp));
+
+    http_network_fingerprint gen{"id", {}, {}, false, true};
+
+    ddwaf::timer deadline{2s};
+    processor_cache cache;
+    auto [output, attr] = gen.eval_impl({{}, {}, false, &headers}, cache, deadline);
+    EXPECT_EQ(output.type, DDWAF_OBJ_STRING);
+    EXPECT_EQ(attr, object_store::attribute::none);
+
+    std::string_view output_sv{
+        output.stringValue, static_cast<std::size_t>(static_cast<std::size_t>(output.nbEntries))};
+    EXPECT_STRV(output_sv, "net-0-1111111111");
+
+    ddwaf_object_free(&headers);
+    ddwaf_object_free(&output);
+}
+
 TEST(TestHttpNetworkFingerprint, AllXFFHeadersRandomChosenHeader)
 {
     ddwaf_object tmp;
@@ -1023,6 +1250,157 @@ TEST(TestSessionFingerprint, CookieValuesNormalization)
     ddwaf_object_free(&output);
 }
 
+TEST(TestSessionFingerprint, CookieValuesAsArray)
+{
+    ddwaf_object tmp;
+
+    ddwaf_object cookies;
+    ddwaf_object_map(&cookies);
+
+    ddwaf_object array;
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "albert,martinez"));
+    ddwaf_object_map_add(&cookies, "name", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "dark"));
+    ddwaf_object_map_add(&cookies, "theme", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "en-GB,en-US"));
+    ddwaf_object_map_add(&cookies, "language", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "xyzabc"));
+    ddwaf_object_map_add(&cookies, "tracking_id", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, ",yes"));
+    ddwaf_object_map_add(&cookies, "gdpr_consent", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "ansd0182u2n,"));
+    ddwaf_object_map_add(&cookies, "session_id", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "2024-07-16T12:00:00Z"));
+    ddwaf_object_map_add(&cookies, "last_visit", &array);
+
+    session_fingerprint gen{"id", {}, {}, false, true};
+
+    ddwaf::timer deadline{2s};
+    processor_cache cache;
+    auto [output, attr] = gen.eval_impl({{{}, {}, false, &cookies}},
+        {{{}, {}, false, "ansd0182u2n"}}, {{{}, {}, false, "admin"}}, cache, deadline);
+
+    EXPECT_EQ(output.type, DDWAF_OBJ_STRING);
+    EXPECT_EQ(attr, object_store::attribute::none);
+
+    std::string_view output_sv{
+        output.stringValue, static_cast<std::size_t>(static_cast<std::size_t>(output.nbEntries))};
+    EXPECT_STRV(output_sv, "ssn-8c6976e5-df6143bc-64f82cf7-269500d3");
+
+    ddwaf_object_free(&cookies);
+    ddwaf_object_free(&output);
+}
+
+TEST(TestSessionFingerprint, CookieValuesAsArrayInvalidType)
+{
+    ddwaf_object tmp;
+
+    ddwaf_object cookies;
+    ddwaf_object_map(&cookies);
+
+    ddwaf_object array;
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_unsigned(&tmp, 42));
+    ddwaf_object_map_add(&cookies, "name", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_unsigned(&tmp, 42));
+    ddwaf_object_map_add(&cookies, "theme", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_unsigned(&tmp, 42));
+    ddwaf_object_map_add(&cookies, "language", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_unsigned(&tmp, 42));
+    ddwaf_object_map_add(&cookies, "tracking_id", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_unsigned(&tmp, 42));
+    ddwaf_object_map_add(&cookies, "gdpr_consent", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_unsigned(&tmp, 42));
+    ddwaf_object_map_add(&cookies, "session_id", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_unsigned(&tmp, 42));
+    ddwaf_object_map_add(&cookies, "last_visit", &array);
+
+    session_fingerprint gen{"id", {}, {}, false, true};
+
+    ddwaf::timer deadline{2s};
+    processor_cache cache;
+    auto [output, attr] = gen.eval_impl({{{}, {}, false, &cookies}},
+        {{{}, {}, false, "ansd0182u2n"}}, {{{}, {}, false, "admin"}}, cache, deadline);
+
+    EXPECT_EQ(output.type, DDWAF_OBJ_STRING);
+    EXPECT_EQ(attr, object_store::attribute::none);
+
+    std::string_view output_sv{
+        output.stringValue, static_cast<std::size_t>(static_cast<std::size_t>(output.nbEntries))};
+    EXPECT_STRV(output_sv, "ssn-8c6976e5-df6143bc-d3648ef2-269500d3");
+
+    ddwaf_object_free(&cookies);
+    ddwaf_object_free(&output);
+}
+
+TEST(TestSessionFingerprint, CookieValuesArrayMultiples)
+{
+    ddwaf_object tmp;
+
+    ddwaf_object cookies;
+    ddwaf_object_map(&cookies);
+
+    ddwaf_object array;
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "albert,martinez"));
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "albert,martinez"));
+    ddwaf_object_map_add(&cookies, "name", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "dark"));
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "dark"));
+    ddwaf_object_map_add(&cookies, "theme", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "en-GB,en-US"));
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "en-GB,en-US"));
+    ddwaf_object_map_add(&cookies, "language", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "xyzabc"));
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "xyzabc"));
+    ddwaf_object_map_add(&cookies, "tracking_id", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, ",yes"));
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, ",yes"));
+    ddwaf_object_map_add(&cookies, "gdpr_consent", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "ansd0182u2n,"));
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "ansd0182u2n,"));
+    ddwaf_object_map_add(&cookies, "session_id", &array);
+    ddwaf_object_array(&array);
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "2024-07-16T12:00:00Z"));
+    ddwaf_object_array_add(&array, ddwaf_object_string(&tmp, "2024-07-16T12:00:00Z"));
+    ddwaf_object_map_add(&cookies, "last_visit", &array);
+
+    session_fingerprint gen{"id", {}, {}, false, true};
+
+    ddwaf::timer deadline{2s};
+    processor_cache cache;
+    auto [output, attr] = gen.eval_impl({{{}, {}, false, &cookies}},
+        {{{}, {}, false, "ansd0182u2n"}}, {{{}, {}, false, "admin"}}, cache, deadline);
+
+    EXPECT_EQ(output.type, DDWAF_OBJ_STRING);
+    EXPECT_EQ(attr, object_store::attribute::none);
+
+    std::string_view output_sv{
+        output.stringValue, static_cast<std::size_t>(static_cast<std::size_t>(output.nbEntries))};
+    EXPECT_STRV(output_sv, "ssn-8c6976e5-df6143bc-d3648ef2-269500d3");
+
+    ddwaf_object_free(&cookies);
+    ddwaf_object_free(&output);
+}
+
 TEST(TestSessionFingerprint, CookieEmptyValues)
 {
     ddwaf_object tmp;


Reviews:
user: login: Taiki-San
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-27T15:35:41Z
user: login: cataphract
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T15:39:26Z
user: login: cataphract
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T15:53:14Z

Review comments:
diff_hunk: @@ -334,6 +334,10 @@ struct kv_hash_fields : field_generator<kv_hash_fields, std::pair<std::string, s
             if (child.type == DDWAF_OBJ_STRING) {
                 val =
                     std::string_view{child.stringValue, static_cast<std::size_t>(child.nbEntries)};
+            } else if (child.type == DDWAF_OBJ_ARRAY && child.nbEntries == 1) {
+                const auto &grandchild = child.array[0];
+                val = std::string_view{
path: src/processor/fingerprint.cpp
user: login: cataphract
body: how can you guarantee that grandchild is a string?
created_at: 2025-03-27T15:38:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -612,8 +616,14 @@ std::pair<ddwaf_object, object_store::attribute> http_header_fingerprint::eval_i
             known_header_bitset[index] = '1';
         } else if (type == header_type::unknown) {
             unknown_headers.emplace_back(normalized_header);
-        } else if (type == header_type::user_agent && child.type == DDWAF_OBJ_STRING) {
-            user_agent = {child.stringValue, static_cast<std::size_t>(child.nbEntries)};
+        } else if (type == header_type::user_agent) {
+            if (child.type == DDWAF_OBJ_STRING) {
+                user_agent = {child.stringValue, static_cast<std::size_t>(child.nbEntries)};
+            } else if (child.type == DDWAF_OBJ_ARRAY && child.nbEntries == 1) {
path: src/processor/fingerprint.cpp
user: login: cataphract
body: same
created_at: 2025-03-27T15:38:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -664,10 +674,17 @@ std::pair<ddwaf_object, object_store::attribute> http_network_fingerprint::eval_
             // Verify not only precedence but also type, as a header of an unexpected
             // type will be unlikely to be used unless the framework has somehow
             // broken down the header into constituent IPs
-            if (chosen_header > index && child.type == DDWAF_OBJ_STRING) {
-                chosen_header_value = {
-                    child.stringValue, static_cast<std::size_t>(child.nbEntries)};
-                chosen_header = index;
+            if (chosen_header > index) {
+                if (child.type == DDWAF_OBJ_STRING) {
+                    chosen_header_value = {
+                        child.stringValue, static_cast<std::size_t>(child.nbEntries)};
+                    chosen_header = index;
+                } else if (child.type == DDWAF_OBJ_ARRAY && child.nbEntries == 1) {
+                    const auto &grandchild = child.array[0];
path: src/processor/fingerprint.cpp
user: login: cataphract
body: same
created_at: 2025-03-27T15:39:24Z
author_association: CONTRIBUTOR

PR comments:
user: login: pr-commenter[bot]
created_at: 2025-03-27T14:43:48Z
body: ## Benchmarks clang

Benchmark execution time: 2025-03-27 17:17:20

Comparing candidate commit 688340a4 in PR branch `anilm3/fingerprint-support-array-scalars` with baseline commit 9c9d74e5 in branch `master`.

Found 1 performance improvements and 0 performance regressions! Performance is the same for 0 metrics, 0 unstable metrics.

#### scenario:global-benchmark.random.clang

- 🟩 `execution_time` [-18.220ms; -18.200ms] or [-7.090%; -7.082%]




author_association: NONE
user: login: pr-commenter[bot]
created_at: 2025-03-27T14:46:31Z
body: ## Benchmarks gcc

Benchmark execution time: 2025-03-27 17:20:32

Comparing candidate commit 688340a4 in PR branch `anilm3/fingerprint-support-array-scalars` with baseline commit 9c9d74e5 in branch `master`.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 1 metrics, 0 unstable metrics.


author_association: NONE
user: login: pr-commenter[bot]
created_at: 2025-03-27T14:58:24Z
body: ## Benchmarks clang-pgo

Benchmark execution time: 2025-03-27 17:32:31

Comparing candidate commit 688340a4 in PR branch `anilm3/fingerprint-support-array-scalars` with baseline commit 9c9d74e5 in branch `master`.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 1 metrics, 0 unstable metrics.


author_association: NONE
user: login: codecov-commenter
created_at: 2025-03-27T15:00:42Z
body: ## [Codecov](https://app.codecov.io/gh/DataDog/libddwaf/pull/392?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog) Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 85.10%. Comparing base ([`9c9d74e`](https://app.codecov.io/gh/DataDog/libddwaf/commit/9c9d74e5f7d009b03d0d30c0ae24efe0d14510ec?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog)) to head ([`688340a`](https://app.codecov.io/gh/DataDog/libddwaf/commit/688340a409188a651e0c701d2f83f85e4ff01b8b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog)).
:warning: Report is 66 commits behind head on master.

<details><summary>Additional details and impacted files</summary>



```diff
@@            Coverage Diff             @@
##           master     #392      +/-   ##
==========================================
+ Coverage   85.08%   85.10%   +0.02%     
==========================================
  Files         164      164              
  Lines        8274     8279       +5     
  Branches     3627     3630       +3     
==========================================
+ Hits         7040     7046       +6     
  Misses        471      471              
+ Partials      763      762       -1     
```

| [Flag](https://app.codecov.io/gh/DataDog/libddwaf/pull/392/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog) | Coverage Δ | |
|---|---|---|
| [waf_test](https://app.codecov.io/gh/DataDog/libddwaf/pull/392/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog) | `85.10% <100.00%> (+0.02%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog#carryforward-flags-in-the-pull-request-comment) to find out more.
</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/DataDog/libddwaf/pull/392?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog).
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE