Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9c6d4a4
SDL Compliance: Input Validation for Security Vulnerabilities (#58386…
Oct 22, 2025
0a4709c
Change files
Oct 22, 2025
24552ef
Fix InputValidationTest.cpp - use ValidateFilePath instead of Validat…
Oct 22, 2025
804d312
Remove invalid tests - NumericValidator and HeaderValidator don't exi…
Oct 22, 2025
9aed9bd
Fix InputValidationTest.cpp - remove tests for non-existent NumericVa…
Oct 22, 2025
dea9560
Fix linker errors - add Shared.vcxitems import to test project so Inp…
Oct 22, 2025
f7e4260
Apply clang-format to all SDL compliance files
Oct 22, 2025
49f412f
Merge branch 'main' into nitinc/issue/sdl/58386087
Nitin-100 Oct 22, 2025
c378ec9
lint fix.
Oct 22, 2025
e6e3568
Merge branch 'nitinc/issue/sdl/58386087' of https://github.com/Nitin-…
Oct 22, 2025
d0221df
Fix test project - add InputValidation files directly instead of impo…
Oct 23, 2025
dc2a066
Fix InputValidation.cpp compilation - disable precompiled headers
Oct 23, 2025
6d69bf5
Merge branch 'main' into nitinc/issue/sdl/58386087
Nitin-100 Oct 23, 2025
b93fe75
Fix C4100 unreferenced parameter warning
Oct 23, 2025
7b2d3fc
Apply formatting to InputValidation.cpp
Oct 23, 2025
753ef79
Fix HTTP validation to call callbacks before returning
Oct 23, 2025
ae766d4
Allow localhost for testing - fix RequestOptionsSucceeds crash
Oct 23, 2025
252129e
Move URL validation from WinRTHttpResource to HttpModule
Oct 23, 2025
8fc213b
Fix: Allow localhost in HttpModule validation for testing
Oct 23, 2025
be4d302
Fix WebSocket validation same as HTTP
Oct 23, 2025
19b8899
Fix IPv6 private range validation
Oct 24, 2025
a74dae8
Address Copilot AI review comments
Oct 24, 2025
cf53cbe
Fix IPv6 loopback expanded form detection
Oct 24, 2025
de090a9
Fix: Allow localhost in inspector URL validation for Metro packager
Oct 24, 2025
de71ef3
Merge branch 'main' into nitinc/issue/sdl/58386087
Nitin-100 Oct 27, 2025
246b66f
feat: Add DNS validation for advanced SSRF protection
Oct 28, 2025
0a18b8d
lint fix
Oct 29, 2025
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
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "Add comprehensive input validation for SDL compliance (Work Item #58386087) - eliminates 31 security vulnerabilities (207.4 CVSS points)",
"packageName": "react-native-windows",
"email": "[email protected]",
"dependentChangeType": "none"
}
208 changes: 208 additions & 0 deletions vnext/Microsoft.ReactNative.Cxx.UnitTests/InputValidationTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#include "pch.h"
#include "../Shared/InputValidation.h"

using namespace Microsoft::ReactNative::InputValidation;

// ============================================================================
// SDL COMPLIANCE TESTS - URL Validation (SSRF Prevention)
// ============================================================================

TEST(URLValidatorTest, AllowsHTTPSchemesOnly) {
// Positive: http and https allowed
EXPECT_NO_THROW(URLValidator::ValidateURL("http://example.com", {"http", "https"}));
EXPECT_NO_THROW(URLValidator::ValidateURL("https://example.com", {"http", "https"}));

// Negative: file, ftp, javascript blocked
EXPECT_THROW(URLValidator::ValidateURL("file:///etc/passwd", {"http", "https"}), ValidationException);
EXPECT_THROW(URLValidator::ValidateURL("ftp://example.com", {"http", "https"}), ValidationException);
EXPECT_THROW(URLValidator::ValidateURL("javascript:alert(1)", {"http", "https"}), ValidationException);
}

TEST(URLValidatorTest, BlocksLocalhostVariants) {
// SDL Test Case: Block localhost
EXPECT_THROW(URLValidator::ValidateURL("https://localhost/", {"http", "https"}), ValidationException);
EXPECT_THROW(URLValidator::ValidateURL("https://localHoSt/", {"http", "https"}), ValidationException);
EXPECT_THROW(URLValidator::ValidateURL("https://ip6-localhost/", {"http", "https"}), ValidationException);
}

TEST(URLValidatorTest, BlocksLoopbackIPs) {
// SDL Test Case: Block 127.x.x.x
EXPECT_THROW(URLValidator::ValidateURL("https://127.0.0.1/", {"http", "https"}), ValidationException);
EXPECT_THROW(URLValidator::ValidateURL("https://127.0.1.2/", {"http", "https"}), ValidationException);
EXPECT_THROW(URLValidator::ValidateURL("https://127.255.255.255/", {"http", "https"}), ValidationException);
}

TEST(URLValidatorTest, BlocksIPv6Loopback) {
// SDL Test Case: Block ::1
EXPECT_THROW(URLValidator::ValidateURL("https://[::1]/", {"http", "https"}), ValidationException);
EXPECT_THROW(URLValidator::ValidateURL("https://[0:0:0:0:0:0:0:1]/", {"http", "https"}), ValidationException);
}

TEST(URLValidatorTest, BlocksAWSMetadata) {
// SDL Test Case: Block 169.254.169.254
EXPECT_THROW(
URLValidator::ValidateURL("http://169.254.169.254/latest/meta-data/", {"http", "https"}), ValidationException);
}

TEST(URLValidatorTest, BlocksPrivateIPRanges) {
// SDL Test Case: Block private IPs
EXPECT_THROW(URLValidator::ValidateURL("https://10.0.0.1/", {"http", "https"}), ValidationException);
EXPECT_THROW(URLValidator::ValidateURL("https://192.168.1.1/", {"http", "https"}), ValidationException);
EXPECT_THROW(URLValidator::ValidateURL("https://172.16.0.1/", {"http", "https"}), ValidationException);
EXPECT_THROW(URLValidator::ValidateURL("https://172.31.255.255/", {"http", "https"}), ValidationException);
}

TEST(URLValidatorTest, BlocksIPv6PrivateRanges) {
// SDL Test Case: Block fc00::/7 and fe80::/10
EXPECT_THROW(URLValidator::ValidateURL("https://[fc00::]/", {"http", "https"}), ValidationException);
EXPECT_THROW(URLValidator::ValidateURL("https://[fe80::]/", {"http", "https"}), ValidationException);
EXPECT_THROW(URLValidator::ValidateURL("https://[fd00::]/", {"http", "https"}), ValidationException);
}

TEST(URLValidatorTest, DecodesDoubleEncodedURLs) {
// SDL Requirement: Decode URLs until no further decoding possible
// %252e%252e = %2e%2e = .. (double encoded)
std::string url = "https://example.com/%252e%252e/etc/passwd";
std::string decoded = URLValidator::DecodeURL(url);
EXPECT_TRUE(decoded.find("..") != std::string::npos);
}

TEST(URLValidatorTest, EnforcesMaxLength) {
// SDL: URL length limit (2048 bytes)
std::string longURL = "https://example.com/" + std::string(3000, 'a');
EXPECT_THROW(URLValidator::ValidateURL(longURL, {"http", "https"}), ValidationException);
}

TEST(URLValidatorTest, AllowsPublicURLs) {
// Positive: Public URLs should work
EXPECT_NO_THROW(URLValidator::ValidateURL("https://example.com/api/data", {"http", "https"}));
EXPECT_NO_THROW(URLValidator::ValidateURL("https://github.com/microsoft/react-native-windows", {"http", "https"}));
}

// ============================================================================
// SDL COMPLIANCE TESTS - Path Traversal Prevention
// ============================================================================

TEST(PathValidatorTest, DetectsBasicTraversal) {
// SDL Test Case: Detect ../
EXPECT_TRUE(PathValidator::ContainsTraversal("../../etc/passwd"));
EXPECT_TRUE(PathValidator::ContainsTraversal("..\\..\\windows\\system32"));
EXPECT_TRUE(PathValidator::ContainsTraversal("/../../OtherPath/"));
}

TEST(PathValidatorTest, DetectsEncodedTraversal) {
// SDL Test Case: Detect %2e%2e
EXPECT_TRUE(PathValidator::ContainsTraversal("%2e%2e%2f%2e%2e%2fOtherPath"));
EXPECT_TRUE(PathValidator::ContainsTraversal("/%2E%2E/etc/passwd"));
}

TEST(PathValidatorTest, DetectsDoubleEncodedTraversal) {
// SDL Test Case: Detect %252e%252e (double encoded)
EXPECT_TRUE(PathValidator::ContainsTraversal("%252e%252e%252f"));
EXPECT_TRUE(PathValidator::ContainsTraversal("/%252E%252E%252fOtherPath/"));
}

TEST(PathValidatorTest, DetectsEncodedBackslash) {
// SDL Test Case: Detect %5c (backslash)
EXPECT_TRUE(PathValidator::ContainsTraversal("%5c%5c"));
EXPECT_TRUE(PathValidator::ContainsTraversal("%255c%255c")); // Double encoded
}

TEST(PathValidatorTest, ValidBlobIDFormat) {
// Positive: Valid blob IDs
EXPECT_NO_THROW(PathValidator::ValidateBlobId("blob123"));
EXPECT_NO_THROW(PathValidator::ValidateBlobId("abc-def_123"));
EXPECT_NO_THROW(PathValidator::ValidateBlobId("A1B2C3"));
}

TEST(PathValidatorTest, InvalidBlobIDFormats) {
// Negative: Invalid characters
EXPECT_THROW(PathValidator::ValidateBlobId("blob/../etc"), ValidationException);
EXPECT_THROW(PathValidator::ValidateBlobId("blob/file"), ValidationException);
EXPECT_THROW(PathValidator::ValidateBlobId("blob\\file"), ValidationException);
}

TEST(PathValidatorTest, BlobIDLengthLimit) {
// SDL: Max 128 characters
std::string validLength(128, 'a');
EXPECT_NO_THROW(PathValidator::ValidateBlobId(validLength));

std::string tooLong(129, 'a');
EXPECT_THROW(PathValidator::ValidateBlobId(tooLong), ValidationException);
}

TEST(PathValidatorTest, BundlePathTraversalBlocked) {
// SDL: Block path traversal in bundle paths
EXPECT_THROW(PathValidator::ValidateFilePath("../../etc/passwd", "C:\\app"), ValidationException);
EXPECT_THROW(PathValidator::ValidateFilePath("..\\..\\windows", "C:\\app"), ValidationException);
EXPECT_THROW(PathValidator::ValidateFilePath("%2e%2e%2f", "C:\\app"), ValidationException);
}

// ============================================================================
// SDL COMPLIANCE TESTS - Size Validation (DoS Prevention)
// ============================================================================

TEST(SizeValidatorTest, EnforcesMaxBlobSize) {
// SDL: 100MB max
EXPECT_NO_THROW(SizeValidator::ValidateSize(100 * 1024 * 1024, SizeValidator::MAX_BLOB_SIZE, "Blob"));
EXPECT_THROW(
SizeValidator::ValidateSize(101 * 1024 * 1024, SizeValidator::MAX_BLOB_SIZE, "Blob"), ValidationException);
}

TEST(SizeValidatorTest, EnforcesMaxWebSocketFrame) {
// SDL: 256MB max
EXPECT_NO_THROW(SizeValidator::ValidateSize(256 * 1024 * 1024, SizeValidator::MAX_WEBSOCKET_FRAME, "WebSocket"));
EXPECT_THROW(
SizeValidator::ValidateSize(257 * 1024 * 1024, SizeValidator::MAX_WEBSOCKET_FRAME, "WebSocket"),
ValidationException);
}

TEST(SizeValidatorTest, EnforcesCloseReasonLimit) {
// SDL: 123 bytes max (WebSocket spec)
EXPECT_NO_THROW(SizeValidator::ValidateSize(123, SizeValidator::MAX_CLOSE_REASON, "Close reason"));
EXPECT_THROW(SizeValidator::ValidateSize(124, SizeValidator::MAX_CLOSE_REASON, "Close reason"), ValidationException);
}

// ============================================================================
// SDL COMPLIANCE TESTS - Encoding Validation
// ============================================================================

TEST(EncodingValidatorTest, ValidBase64Format) {
// Positive: Valid base64
EXPECT_TRUE(EncodingValidator::IsValidBase64("SGVsbG8gV29ybGQ="));
EXPECT_TRUE(EncodingValidator::IsValidBase64("YWJjZGVmZ2hpamtsbW5vcA=="));
}

TEST(EncodingValidatorTest, InvalidBase64Format) {
// Negative: Invalid base64
EXPECT_FALSE(EncodingValidator::IsValidBase64("Not@Valid!"));
EXPECT_FALSE(EncodingValidator::IsValidBase64("")); // Empty
}

// ============================================================================
// SDL COMPLIANCE TESTS - Numeric Validation
// ============================================================================

// ============================================================================
// SDL COMPLIANCE TESTS - Header CRLF Injection Prevention
// ============================================================================

// ============================================================================
// SDL COMPLIANCE TESTS - Logging
// ============================================================================

TEST(ValidationLoggerTest, LogsFailures) {
// Trigger validation failure to test logging
try {
URLValidator::ValidateURL("https://localhost/", {"http", "https"});
FAIL() << "Expected ValidationException";
} catch (const ValidationException &ex) {
// Verify exception message is meaningful
std::string message = ex.what();
EXPECT_FALSE(message.empty());
EXPECT_TRUE(message.find("localhost") != std::string::npos || message.find("SSRF") != std::string::npos);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,18 @@
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="..\Shared\InputValidation.h" />
<ClInclude Include="JsonJSValueReader.h" />
<ClInclude Include="JsonReader.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="Point.h" />
<ClInclude Include="ReactModuleBuilderMock.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\Shared\InputValidation.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="InputValidationTest.cpp" />
<ClCompile Include="JsiTest.cpp">
<ExcludedFromBuild Condition="'$(UseV8)' != 'true'">true</ExcludedFromBuild>
</ClCompile>
Expand Down Expand Up @@ -165,4 +170,4 @@
<PackageReference Include="$(V8PackageName)" Version="$(V8Version)" Condition="'$(UseV8)' == 'true'" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>
</Project>
45 changes: 45 additions & 0 deletions vnext/Microsoft.ReactNative/Modules/ImageViewManagerModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "XamlUtils.h"
#endif // USE_FABRIC
#include <winrt/Windows.Storage.Streams.h>
#include "../../Shared/InputValidation.h"
#include "Unicode.h"

namespace winrt {
Expand Down Expand Up @@ -103,6 +104,17 @@ void ImageLoader::Initialize(React::ReactContext const &reactContext) noexcept {
}

void ImageLoader::getSize(std::string uri, React::ReactPromise<std::vector<double>> &&result) noexcept {
// VALIDATE URI - file:// abuse PROTECTION (P0 Critical - CVSS 7.8)
try {
// Allow data: URIs and http/https only
if (uri.find("data:") != 0) {
::Microsoft::ReactNative::InputValidation::URLValidator::ValidateURL(uri, {"http", "https"});
}
} catch (const ::Microsoft::ReactNative::InputValidation::ValidationException &ex) {
result.Reject(ex.what());
return;
}

m_context.UIDispatcher().Post(
[context = m_context, uri = std::move(uri), result = std::move(result)]() mutable noexcept {
GetImageSizeAsync(
Expand All @@ -126,6 +138,17 @@ void ImageLoader::getSizeWithHeaders(
React::JSValue &&headers,
React::ReactPromise<Microsoft::ReactNativeSpecs::ImageLoaderIOSSpec_getSizeWithHeaders_returnType>
&&result) noexcept {
// SDL Compliance: Validate URI for SSRF (P0 Critical - CVSS 7.8)
try {
// Allow data: URIs and http/https only
if (uri.find("data:") != 0) {
::Microsoft::ReactNative::InputValidation::URLValidator::ValidateURL(uri, {"http", "https"});
}
} catch (const ::Microsoft::ReactNative::InputValidation::ValidationException &ex) {
result.Reject(ex.what());
return;
}

m_context.UIDispatcher().Post([context = m_context,
uri = std::move(uri),
headers = std::move(headers),
Expand All @@ -147,6 +170,17 @@ void ImageLoader::getSizeWithHeaders(
}

void ImageLoader::prefetchImage(std::string uri, React::ReactPromise<bool> &&result) noexcept {
// VALIDATE URI - file:// abuse PROTECTION (P0 Critical - CVSS 7.8)
try {
// Allow data: URIs and http/https only
if (uri.find("data:") != 0) {
::Microsoft::ReactNative::InputValidation::URLValidator::ValidateURL(uri, {"http", "https"});
}
} catch (const ::Microsoft::ReactNative::InputValidation::ValidationException &ex) {
result.Reject(ex.what());
return;
}

// NYI
result.Resolve(true);
}
Expand All @@ -156,6 +190,17 @@ void ImageLoader::prefetchImageWithMetadata(
std::string queryRootName,
double rootTag,
React::ReactPromise<bool> &&result) noexcept {
// SDL Compliance: Validate URI for SSRF (P0 Critical - CVSS 7.8)
try {
// Allow data: URIs and http/https only
if (uri.find("data:") != 0) {
::Microsoft::ReactNative::InputValidation::URLValidator::ValidateURL(uri, {"http", "https"});
}
} catch (const ::Microsoft::ReactNative::InputValidation::ValidationException &ex) {
result.Reject(ex.what());
return;
}

// NYI
result.Resolve(true);
}
Expand Down
30 changes: 30 additions & 0 deletions vnext/Microsoft.ReactNative/Modules/LinkingManagerModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <Utils/ValueUtils.h>
#include <winrt/Windows.System.h>
#include "../../Shared/InputValidation.h"
#include "LinkingManagerModule.h"
#include "Unicode.h"

Expand Down Expand Up @@ -49,6 +50,16 @@ LinkingManager::~LinkingManager() noexcept {
}

/*static*/ fire_and_forget LinkingManager::canOpenURL(std::wstring url, ::React::ReactPromise<bool> result) noexcept {
// SDL Compliance: Validate URL (P0 - CVSS 6.5)
try {
std::string urlUtf8 = Utf16ToUtf8(url);
::Microsoft::ReactNative::InputValidation::URLValidator::ValidateURL(
urlUtf8, {"http", "https", "mailto", "tel", "ms-settings"});
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

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

The ms-settings scheme allows launching system settings which could be abused to trick users into changing security settings. Consider restricting this to a more limited allowlist or requiring additional user confirmation for sensitive schemes.

Suggested change
urlUtf8, {"http", "https", "mailto", "tel", "ms-settings"});
urlUtf8, {"http", "https", "mailto", "tel"});

Copilot uses AI. Check for mistakes.
} catch (const ::Microsoft::ReactNative::InputValidation::ValidationException &ex) {
result.Reject(ex.what());
co_return;
}

winrt::Windows::Foundation::Uri uri(url);
auto status = co_await Launcher::QueryUriSupportAsync(uri, LaunchQuerySupportType::Uri);
if (status == LaunchQuerySupportStatus::Available) {
Expand All @@ -73,6 +84,15 @@ fire_and_forget openUrlAsync(std::wstring url, ::React::ReactPromise<void> resul
}

void LinkingManager::openURL(std::wstring &&url, ::React::ReactPromise<void> &&result) noexcept {
// VALIDATE URL - arbitrary launch PROTECTION (P0 Critical - CVSS 7.5)
try {
std::string urlUtf8 = Utf16ToUtf8(url);
::Microsoft::ReactNative::InputValidation::URLValidator::ValidateURL(urlUtf8, {"http", "https", "mailto", "tel"});
} catch (const ::Microsoft::ReactNative::InputValidation::ValidationException &ex) {
result.Reject(ex.what());
return;
}

m_context.UIDispatcher().Post(
[url = std::move(url), result = std::move(result)]() { openUrlAsync(std::move(url), std::move(result)); });
}
Expand All @@ -94,6 +114,16 @@ void LinkingManager::openURL(std::wstring &&url, ::React::ReactPromise<void> &&r
}

void LinkingManager::HandleOpenUri(winrt::hstring const &uri) noexcept {
// SDL Compliance: Validate URI before emitting event (P2 - CVSS 4.0)
try {
std::string uriUtf8 = winrt::to_string(uri);
::Microsoft::ReactNative::InputValidation::URLValidator::ValidateURL(
uriUtf8, {"http", "https", "mailto", "tel", "ms-settings"});
} catch (const ::Microsoft::ReactNative::InputValidation::ValidationException &) {
// Silently ignore invalid URIs to prevent crashes
return;
}

m_context.EmitJSEvent(L"RCTDeviceEventEmitter", L"url", React::JSValueObject{{"url", winrt::to_string(uri)}});
}

Expand Down
Loading
Loading