Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ Value HTTPRequestManagerModuleFactory::loadModule() {

const auto& requestObject = callContext.getParameter(0);
auto url = requestObject.getMapValue("url").toStringBox();

// Validate URL to prevent SSRF attacks
if (!HTTPRequestManagerUtils::isUrlAllowed(url)) {
callContext.getExceptionTracker().onError(
Error("URL not allowed: blocked localhost, private IP, or cloud metadata"));
return Value::undefined();
}

auto method = requestObject.getMapValue("method").toStringBox();
auto headers = requestObject.getMapValue("headers");
auto body = requestObject.getMapValue("body").getTypedArrayRef();
Expand Down
79 changes: 79 additions & 0 deletions valdi/src/valdi/runtime/Utils/HTTPRequestManagerUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

#include "valdi/runtime/Utils/HTTPRequestManagerUtils.hpp"
#include "valdi_core/cpp/Utils/StringCache.hpp"
#include <string_view>
#include <algorithm>
#include <cctype>
#include <string>

namespace Valdi {

Expand All @@ -32,4 +36,79 @@ std::shared_ptr<snap::valdi_core::HTTPRequestManagerCompletion> HTTPRequestManag
return Valdi::makeShared<HTTPRequestManagerCompletionWithFunction>(std::move(function));
}

bool HTTPRequestManagerUtils::isUrlAllowed(const StringBox& url) {
if (url.isEmpty()) {
return false;
}

std::string_view urlView = url.toStringView();

std::string urlLower;
urlLower.reserve(urlView.size());
std::transform(urlView.begin(), urlView.end(), std::back_inserter(urlLower),
[](unsigned char c) { return std::tolower(c); });
std::string_view urlLowerView(urlLower);

if (urlLowerView.find("http://") != 0 && urlLowerView.find("https://") != 0) {
return false;
}

size_t schemeEnd = urlLowerView.find("://");
if (schemeEnd == std::string_view::npos) {
return false;
}
schemeEnd += 3;

size_t hostEnd = urlLowerView.find_first_of("/?#", schemeEnd);
std::string_view hostPart = hostEnd == std::string_view::npos
? urlLowerView.substr(schemeEnd)
: urlLowerView.substr(schemeEnd, hostEnd - schemeEnd);

size_t portStart = hostPart.find(':');
if (portStart != std::string_view::npos) {
hostPart = hostPart.substr(0, portStart);
}

if (hostPart == "localhost" ||
hostPart == "127.0.0.1" ||
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i hit 127.0.0.2 and the entire function is bypassed

hostPart == "::1" ||
Copy link

@indexds indexds Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

many many many ways to hit ipv6 loopback without explicitly typing ::1, you want to reverse this and only allow the 2000::/3 range

hostPart == "[::1]" ||
hostPart.find("localhost") == 0) {
return false;
}

if (hostPart == "metadata.google.internal" ||
hostPart == "169.254.169.254") {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should block the entire /16 range

return false;
}

if (hostPart.length() >= 3 && hostPart.substr(0, 3) == "10.") {
if (hostPart.length() > 3 && std::isdigit(hostPart[3])) {
return false;
}
}

if (hostPart.length() >= 7 && hostPart.substr(0, 4) == "172.") {
if (hostPart.length() > 4 && std::isdigit(hostPart[4])) {
size_t secondDot = hostPart.find('.', 4);
if (secondDot != std::string_view::npos && secondDot > 4) {
std::string secondOctetStr(hostPart.substr(4, secondDot - 4));
try {
int secondOctet = std::stoi(secondOctetStr);
if (secondOctet >= 16 && secondOctet <= 31) {
return false;
}
} catch (...) {
}
}
}
}

if (hostPart.length() >= 8 && hostPart.substr(0, 8) == "192.168.") {
return false;
}

return true;
}

} // namespace Valdi
7 changes: 7 additions & 0 deletions valdi/src/valdi/runtime/Utils/HTTPRequestManagerUtils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@
#include "valdi_core/HTTPResponse.hpp"
#include "valdi_core/cpp/Utils/Function.hpp"
#include "valdi_core/cpp/Utils/Result.hpp"
#include "valdi_core/cpp/Utils/StringBox.hpp"

namespace Valdi {

class HTTPRequestManagerUtils {
public:
static std::shared_ptr<snap::valdi_core::HTTPRequestManagerCompletion> makeRequestCompletion(
Function<void(Result<snap::valdi_core::HTTPResponse>)> function);

/**
* Validates that a URL is safe to request, blocking SSRF attack vectors.
* Returns false if the URL should be blocked (localhost, private IPs, cloud metadata, etc.)
*/
static bool isUrlAllowed(const StringBox& url);
};

} // namespace Valdi