mirror of
https://github.com/openappsec/openappsec.git
synced 2025-06-28 16:41:02 +03:00
490 lines
18 KiB
C++
Executable File
490 lines
18 KiB
C++
Executable File
// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved.
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include "user_identifiers_config.h"
|
|
|
|
#include "buffer.h"
|
|
#include "nginx_attachment.h"
|
|
#include "nginx_attachment_opaque.h"
|
|
#include "nginx_parser.h"
|
|
#include "cidrs_data.h"
|
|
|
|
using namespace std;
|
|
|
|
USE_DEBUG_FLAG(D_NGINX_ATTACHMENT_PARSER);
|
|
|
|
static const Buffer header_key("headerkey", 9, Buffer::MemoryType::STATIC);
|
|
static const Buffer jwt("authorization", 13, Buffer::MemoryType::STATIC);
|
|
static const Buffer xff("x-forwarded-for", 15, Buffer::MemoryType::STATIC);
|
|
static const Buffer cookie("cookie", 6, Buffer::MemoryType::STATIC);
|
|
static const Buffer source_ip("sourceip", 8, Buffer::MemoryType::STATIC);
|
|
static const Buffer oauth("_oauth2_proxy", 13, Buffer::MemoryType::STATIC);
|
|
static const Buffer empty_buffer("", 0, Buffer::MemoryType::STATIC);
|
|
|
|
const static string jwt_prefix = "Bearer ";
|
|
|
|
UsersAllIdentifiersConfig::UsersIdentifiersConfig::UsersIdentifiersConfig() : source_identifier(source_ip){};
|
|
|
|
UsersAllIdentifiersConfig::UsersIdentifiersConfig::UsersIdentifiersConfig(const std::string &identifier)
|
|
:
|
|
source_identifier(identifier)
|
|
{}
|
|
|
|
bool
|
|
UsersAllIdentifiersConfig::UsersIdentifiersConfig::operator==(const UsersIdentifiersConfig &other) const
|
|
{
|
|
return source_identifier == other.source_identifier;
|
|
}
|
|
|
|
void
|
|
UsersAllIdentifiersConfig::UsersIdentifiersConfig::load(cereal::JSONInputArchive &ar)
|
|
{
|
|
parseJSONKey<string>("sourceIdentifier", source_identifier, ar);
|
|
parseJSONKey<vector<string>>("identifierValues", identifier_values, ar);
|
|
}
|
|
|
|
bool
|
|
UsersAllIdentifiersConfig::UsersIdentifiersConfig::isEqualSourceIdentifier(const string &other) const
|
|
{
|
|
if (source_identifier.size() != other.size()) return false;
|
|
return equal(
|
|
source_identifier.begin(),
|
|
source_identifier.end(),
|
|
other.begin(),
|
|
[] (char c1, char c2) { return tolower(c1) == tolower(c2); }
|
|
);
|
|
}
|
|
|
|
UsersAllIdentifiersConfig::UsersAllIdentifiersConfig()
|
|
{
|
|
}
|
|
|
|
vector<string>
|
|
UsersAllIdentifiersConfig::getHeaderValuesFromConfig(const string &header_key) const
|
|
{
|
|
for (auto user_identifier : user_identifiers) {
|
|
if (user_identifier.isEqualSourceIdentifier(header_key)) {
|
|
dbgDebug(D_NGINX_ATTACHMENT_PARSER) << "Match source identifier is found";
|
|
return user_identifier.getIdentifierValues();
|
|
}
|
|
}
|
|
return vector<string>();
|
|
}
|
|
|
|
void
|
|
UsersAllIdentifiersConfig::load(cereal::JSONInputArchive &ar)
|
|
{
|
|
vector<UsersIdentifiersConfig> tmp_user_identifiers;
|
|
parseJSONKey<vector<UsersIdentifiersConfig>>("sourceIdentifiers", tmp_user_identifiers, ar);
|
|
|
|
user_identifiers.clear();
|
|
user_identifiers.reserve(tmp_user_identifiers.size() + 1);
|
|
for (auto &identifier : tmp_user_identifiers) {
|
|
if (identifier.getSourceIdentifier() == header_key) {
|
|
for (const auto &header : identifier.getIdentifierValues()) {
|
|
user_identifiers.emplace_back(header);
|
|
}
|
|
} else {
|
|
user_identifiers.push_back(identifier);
|
|
}
|
|
}
|
|
|
|
vector<UsersIdentifiersConfig> default_order = {
|
|
UsersIdentifiersConfig(cookie),
|
|
UsersIdentifiersConfig(jwt),
|
|
UsersIdentifiersConfig(xff)
|
|
};
|
|
|
|
auto last_user_defined_header = find_first_of(
|
|
default_order.rbegin(),
|
|
default_order.rend(),
|
|
user_identifiers.begin(),
|
|
user_identifiers.end()
|
|
);
|
|
if (last_user_defined_header == default_order.rend()) {
|
|
user_identifiers.insert(user_identifiers.end(), default_order.begin(), default_order.end());
|
|
} else {
|
|
auto last_defined_forwards = find(default_order.begin(), default_order.end(), *last_user_defined_header);
|
|
user_identifiers.insert(user_identifiers.end(), last_defined_forwards + 1, default_order.end());
|
|
}
|
|
}
|
|
|
|
static bool
|
|
compareBufferWithoutCase(const Buffer &b1, const Buffer &b2)
|
|
{
|
|
if (b1.size() != b2.size()) return false;
|
|
return equal(b1.begin(), b1.end(), b2.begin(), [] (u_char c1, u_char c2) { return tolower(c1) == tolower(c2); });
|
|
}
|
|
|
|
void
|
|
UsersAllIdentifiersConfig::setIdentifierTopaqueCtx(const HttpHeader &header) const
|
|
{
|
|
if (compareBufferWithoutCase(jwt, header.getKey())) {
|
|
setJWTValuesToOpaqueCtx(header);
|
|
} else if (compareBufferWithoutCase(xff, header.getKey())) {
|
|
setXFFValuesToOpaqueCtx(header, ExtractType::SOURCEIDENTIFIER);
|
|
} else if (compareBufferWithoutCase(cookie, header.getKey())) {
|
|
setCookieValuesToOpaqueCtx(header);
|
|
} else {
|
|
setCustomHeaderToOpaqueCtx(header);
|
|
}
|
|
}
|
|
|
|
bool
|
|
UsersAllIdentifiersConfig::isHigherPriority(const string ¤t_identifier, const string &header_key) const
|
|
{
|
|
for (auto user_identifier : user_identifiers) {
|
|
if (user_identifier.isEqualSourceIdentifier(current_identifier)) return false;
|
|
if (user_identifier.isEqualSourceIdentifier(header_key)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void
|
|
UsersAllIdentifiersConfig::setJWTValuesToOpaqueCtx(const HttpHeader &header) const
|
|
{
|
|
const vector<string> jwt_values = getHeaderValuesFromConfig(header.getKey());
|
|
if (jwt_values.size() == 0) {
|
|
dbgTrace(D_NGINX_ATTACHMENT_PARSER) << "No JWT keys exists in configuration";
|
|
return;
|
|
}
|
|
if (bcmp(header.getValue().data(), jwt_prefix.c_str(), jwt_prefix.size()) != 0) {
|
|
dbgTrace(D_NGINX_ATTACHMENT_PARSER) << "Invalid JWT header, 'Bearer' prefix missing";
|
|
return;
|
|
}
|
|
int start_dot = -1;
|
|
int end_dot = -1;
|
|
for (uint i = 0 ; i < header.getValue().size() ; i++) {
|
|
if (header.getValue()[i] == '.') {
|
|
if (start_dot < 0) {
|
|
start_dot = i;
|
|
} else if (end_dot < 0) {
|
|
end_dot = i;
|
|
}
|
|
}
|
|
}
|
|
if (start_dot < 0 || end_dot < 0) {
|
|
dbgTrace(D_NGINX_ATTACHMENT_PARSER) << "The header does not contain dots";
|
|
return;
|
|
}
|
|
|
|
string jwt_str(
|
|
reinterpret_cast<const char *>(header.getValue().data()),
|
|
start_dot + 1,
|
|
end_dot - start_dot - 1
|
|
);
|
|
I_Encryptor *encryptor = Singleton::Consume<I_Encryptor>::by<NginxParser>();
|
|
auto decoded_jwt = encryptor->base64Decode(jwt_str);
|
|
dbgDebug(D_NGINX_ATTACHMENT_PARSER) << "Base64 decoded JWT: " << decoded_jwt;
|
|
|
|
auto i_transaction_table = Singleton::Consume<I_TableSpecific<SessionID>>::by<NginxAttachment>();
|
|
if (!i_transaction_table || !i_transaction_table->hasState<NginxAttachmentOpaque>()) {
|
|
dbgDebug(D_NGINX_ATTACHMENT_PARSER) << "Can't get the transaction table";
|
|
return;
|
|
}
|
|
NginxAttachmentOpaque &opaque = i_transaction_table->getState<NginxAttachmentOpaque>();
|
|
stringstream ss;
|
|
ss.str(decoded_jwt);
|
|
cereal::JSONInputArchive in_ar(ss);
|
|
for (const string &field_name : jwt_values) {
|
|
try {
|
|
string tmp_val;
|
|
in_ar(cereal::make_nvp(field_name, tmp_val));
|
|
opaque.setSourceIdentifier(header.getKey(), tmp_val);
|
|
dbgDebug(D_NGINX_ATTACHMENT_PARSER)
|
|
<< "Added source identifir to context. Key: "
|
|
<< field_name
|
|
<< ". Value: "
|
|
<< tmp_val;
|
|
return;
|
|
} catch (const cereal::Exception &e) {
|
|
dbgTrace(D_NGINX_ATTACHMENT_PARSER)
|
|
<< "Unable to find value for the key: "
|
|
<< field_name
|
|
<< ". Error: "
|
|
<< e.what();
|
|
}
|
|
}
|
|
}
|
|
|
|
static string
|
|
stripOptionalPort(const string::const_iterator &first, const string::const_iterator &last)
|
|
{
|
|
// Microsoft XFF+IPv6+Port yikes - see also here https://github.com/eclipse/jetty.project/issues/3630
|
|
if (*first == '[') {
|
|
// Possible bracketed IPv6 address such as "[2001:db8::1]" + optional numeric ":<port>"
|
|
auto close_bracket = find(first + 1, last, ']');
|
|
if (close_bracket == last) return string(first, last);
|
|
return string(first+1, close_bracket);
|
|
}
|
|
|
|
auto first_colon = find(first, last, ':');
|
|
if (first_colon == last) return string(first, last);
|
|
|
|
// If there is more than one colon it means its probably IPv6 address without brackets
|
|
auto second_colon = find(first_colon + 1, last, ':');
|
|
if (second_colon != last) return string(first, last);
|
|
|
|
// If there's only one colon it can't be IPv6 and can only be IPv4 with port
|
|
return string(first, first_colon);
|
|
}
|
|
|
|
static vector<string>
|
|
split(const string &str)
|
|
{
|
|
vector<string> elems;
|
|
elems.reserve(str.size() / 8 + 1);
|
|
auto sub_start = str.cbegin(), sub_end = str.cbegin();
|
|
for (auto iter = str.cbegin(); iter != str.cend(); ++iter) {
|
|
if (isspace(*iter)) {
|
|
if (sub_start == iter) {
|
|
++sub_start;
|
|
++sub_end;
|
|
}
|
|
} else if (*iter == ',') {
|
|
if (sub_start != sub_end) {
|
|
elems.push_back(stripOptionalPort(sub_start, sub_end));
|
|
}
|
|
sub_end = iter + 1;
|
|
sub_start = iter + 1;
|
|
} else {
|
|
sub_end = iter + 1;
|
|
}
|
|
}
|
|
|
|
if (sub_start != sub_end) {
|
|
elems.push_back(stripOptionalPort(sub_start, sub_end));
|
|
}
|
|
|
|
return elems;
|
|
}
|
|
|
|
static bool
|
|
isIpTrusted(const string &value, const vector<CIDRSData> &cidr_values)
|
|
{
|
|
if (cidr_values.empty()) return true;
|
|
|
|
for(const auto &cidr_data : cidr_values) {
|
|
if (cidr_data.contains(value)) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Maybe<string>
|
|
UsersAllIdentifiersConfig::parseXForwardedFor(const string &str, ExtractType type) const
|
|
{
|
|
vector<string> header_values = split(str);
|
|
if (header_values.empty()) return genError("No IP found in the xff header list");
|
|
|
|
vector<string> xff_values = getHeaderValuesFromConfig("x-forwarded-for");
|
|
vector<CIDRSData> cidr_values(xff_values.begin(), xff_values.end());
|
|
string last_valid_ip;
|
|
|
|
for (auto it = header_values.rbegin(); it != header_values.rend() - 1; ++it) {
|
|
if (!IPAddr::createIPAddr(*it).ok()) {
|
|
dbgWarning(D_NGINX_ATTACHMENT_PARSER) << "Invalid IP address found in the xff header IPs list: " << *it;
|
|
if (last_valid_ip.empty()) {
|
|
return genError("Invalid IP address");
|
|
}
|
|
return last_valid_ip;
|
|
}
|
|
last_valid_ip = *it;
|
|
if (type == ExtractType::PROXYIP) continue;
|
|
if (!isIpTrusted(*it, cidr_values)) {
|
|
dbgDebug(D_NGINX_ATTACHMENT_PARSER) << "Found untrusted IP in the xff header IPs list: " << *it;
|
|
return *it;
|
|
}
|
|
}
|
|
|
|
if (!IPAddr::createIPAddr(header_values[0]).ok()) {
|
|
dbgWarning(D_NGINX_ATTACHMENT_PARSER)
|
|
<< "Invalid IP address found in the xff header IPs list: "
|
|
<< header_values[0];
|
|
if (last_valid_ip.empty()) {
|
|
return genError("No Valid Ip address was found");
|
|
}
|
|
return last_valid_ip;
|
|
}
|
|
|
|
return header_values[0];
|
|
}
|
|
|
|
|
|
void
|
|
UsersAllIdentifiersConfig::setXFFValuesToOpaqueCtx(const HttpHeader &header, ExtractType type) const
|
|
{
|
|
auto i_transaction_table = Singleton::Consume<I_TableSpecific<SessionID>>::by<NginxAttachment>();
|
|
if (!i_transaction_table || !i_transaction_table->hasState<NginxAttachmentOpaque>()) {
|
|
dbgTrace(D_NGINX_ATTACHMENT_PARSER) << "Can't get the transaction table";
|
|
return;
|
|
}
|
|
NginxAttachmentOpaque &opaque = i_transaction_table->getState<NginxAttachmentOpaque>();
|
|
auto value = parseXForwardedFor(header.getValue(), type);
|
|
if (!value.ok()) {
|
|
dbgTrace(D_NGINX_ATTACHMENT_PARSER) << "Could not extract source identifier from X-Forwarded-For header";
|
|
return;
|
|
};
|
|
|
|
if (type == ExtractType::SOURCEIDENTIFIER) {
|
|
opaque.setSourceIdentifier(header.getKey(), value.unpack());
|
|
dbgDebug(D_NGINX_ATTACHMENT_PARSER)
|
|
<< "Added source identifier from XFF header"
|
|
<< value.unpack();
|
|
opaque.setSavedData(HttpTransactionData::xff_vals_ctx, header.getValue());
|
|
opaque.setSavedData(HttpTransactionData::source_identifier, value.unpack());
|
|
dbgTrace(D_NGINX_ATTACHMENT_PARSER)
|
|
<< "XFF found, set ctx with value from header: "
|
|
<< static_cast<string>(header.getValue());
|
|
} else {
|
|
opaque.setSavedData(HttpTransactionData::proxy_ip_ctx, value.unpack());
|
|
}
|
|
}
|
|
|
|
void
|
|
UsersAllIdentifiersConfig::setCustomHeaderToOpaqueCtx(const HttpHeader &header) const
|
|
{
|
|
auto i_transaction_table = Singleton::Consume<I_TableSpecific<SessionID>>::by<NginxAttachment>();
|
|
if (!i_transaction_table || !i_transaction_table->hasState<NginxAttachmentOpaque>()) {
|
|
dbgDebug(D_NGINX_ATTACHMENT_PARSER) << "Can't get the transaction table";
|
|
return;
|
|
}
|
|
i_transaction_table->getState<NginxAttachmentOpaque>().setSourceIdentifier(header.getKey(), header.getValue());
|
|
dbgDebug(D_NGINX_ATTACHMENT_PARSER)
|
|
<< "Added source identifir to custom header: "
|
|
<< static_cast<string>(header.getValue());
|
|
return;
|
|
}
|
|
|
|
void
|
|
UsersAllIdentifiersConfig::setWafTagValuesToOpaqueCtx(const HttpHeader &header) const
|
|
{
|
|
auto i_transaction_table = Singleton::Consume<I_TableSpecific<SessionID>>::by<NginxAttachment>();
|
|
if (!i_transaction_table || !i_transaction_table->hasState<NginxAttachmentOpaque>()) {
|
|
dbgDebug(D_NGINX_ATTACHMENT_PARSER) << "Can't get the transaction table";
|
|
return;
|
|
}
|
|
|
|
NginxAttachmentOpaque &opaque = i_transaction_table->getState<NginxAttachmentOpaque>();
|
|
opaque.setSavedData(HttpTransactionData::waf_tag_ctx, static_cast<string>(header.getValue()));
|
|
|
|
dbgDebug(D_NGINX_ATTACHMENT_PARSER)
|
|
<< "Added waf tag to context: "
|
|
<< static_cast<string>(header.getValue());
|
|
return;
|
|
}
|
|
|
|
Maybe<string>
|
|
UsersAllIdentifiersConfig::parseCookieElement(
|
|
const string::const_iterator &start,
|
|
const string::const_iterator &end,
|
|
const string &key) const
|
|
{
|
|
auto curr_pos = start;
|
|
|
|
// Skip whitespace
|
|
for (; curr_pos != end && isspace(*curr_pos); ++curr_pos);
|
|
|
|
// Check key
|
|
for (auto key_pos = key.begin(); key_pos != key.end(); ++key_pos) {
|
|
if (curr_pos == end || tolower(*curr_pos) != tolower(*key_pos)) return genError("Key value not found");
|
|
++curr_pos;
|
|
}
|
|
|
|
// Skip whitespace
|
|
for (; curr_pos != end && isspace(*curr_pos); ++curr_pos);
|
|
|
|
// Check for '='
|
|
if (curr_pos == end || *curr_pos != '=') return genError("Equal sign not found");
|
|
++curr_pos;
|
|
|
|
// Skip whitespace
|
|
for (; curr_pos != end && isspace(*curr_pos); ++curr_pos);
|
|
|
|
auto value_start = curr_pos;
|
|
|
|
// Read value
|
|
for (; curr_pos != end && !isspace(*curr_pos); ++curr_pos);
|
|
|
|
auto value_end = curr_pos;
|
|
|
|
// Verify value read currectly - should be only whitespaces to the end;
|
|
for (; curr_pos != end && isspace(*curr_pos); ++curr_pos);
|
|
if (curr_pos != end) return genError("Unexpected characters when reading a value");
|
|
|
|
return string(value_start, value_end);
|
|
}
|
|
|
|
Buffer
|
|
UsersAllIdentifiersConfig::extractKeyValueFromCookie(const string &cookie_value, const string &key) const
|
|
{
|
|
auto curr_start = cookie_value.begin();
|
|
auto end = cookie_value.end();
|
|
|
|
while (curr_start != end) {
|
|
auto curr_end = find(curr_start, end, ';');
|
|
auto res = parseCookieElement(curr_start, curr_end, key);
|
|
if (res.ok()) {
|
|
if (key != oauth) return *res;
|
|
I_Encryptor *encryptor = Singleton::Consume<I_Encryptor>::by<NginxParser>();
|
|
auto decoded_value = encryptor->base64Decode(*res);
|
|
auto decoded_end = find(decoded_value.begin(), decoded_value.end(), '|');
|
|
return Buffer(string(decoded_value.begin(), decoded_end));
|
|
}
|
|
|
|
if (curr_end != end) ++curr_end;
|
|
curr_start = curr_end;
|
|
}
|
|
|
|
return empty_buffer;
|
|
}
|
|
|
|
void
|
|
UsersAllIdentifiersConfig::setCookieValuesToOpaqueCtx(const HttpHeader &header) const
|
|
{
|
|
vector<string> cookie_keys = getHeaderValuesFromConfig(header.getKey());
|
|
cookie_keys.push_back(oauth);
|
|
cookie_keys.push_back("jsessionid");
|
|
for (const string &key : cookie_keys) {
|
|
string value = extractKeyValueFromCookie(header.getValue(), key);
|
|
if (!value.empty()) {
|
|
dbgDebug(D_NGINX_ATTACHMENT_PARSER) << "Set source identifier from cookie: Oauth 2";
|
|
auto i_transaction_table = Singleton::Consume<I_TableSpecific<SessionID>>::by<NginxAttachment>();
|
|
if (!i_transaction_table || !i_transaction_table->hasState<NginxAttachmentOpaque>()) {
|
|
dbgDebug(D_NGINX_ATTACHMENT_PARSER) << "Can't get the transaction table";
|
|
return;
|
|
}
|
|
NginxAttachmentOpaque &opaque = i_transaction_table->getState<NginxAttachmentOpaque>();
|
|
opaque.setSourceIdentifier(header.getKey(), value);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
UsersAllIdentifiersConfig::parseRequestHeaders(const HttpHeader &header) const
|
|
{
|
|
auto i_transaction_table = Singleton::Consume<I_TableSpecific<SessionID>>::by<NginxAttachment>();
|
|
if (!i_transaction_table || !i_transaction_table->hasState<NginxAttachmentOpaque>()) {
|
|
dbgDebug(D_NGINX_ATTACHMENT_PARSER) << "Can't get the transaction table";
|
|
return;
|
|
}
|
|
|
|
NginxAttachmentOpaque &opaque = i_transaction_table->getState<NginxAttachmentOpaque>();
|
|
const string ¤t_identifier = opaque.getSourceIdentifiersType();
|
|
|
|
if (!isHigherPriority(current_identifier, header.getKey())) return;
|
|
|
|
setIdentifierTopaqueCtx(header);
|
|
}
|