Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d41fbe7
SDL Compliance: Input Validation for Security Vulnerabilities (#58386…
Oct 22, 2025
ee47941
Change files
Oct 22, 2025
ed5ffb0
Fix InputValidationTest.cpp - use ValidateFilePath instead of Validat…
Oct 22, 2025
3cb6b04
Remove invalid tests - NumericValidator and HeaderValidator don't exi…
Oct 22, 2025
bf306e8
Fix InputValidationTest.cpp - remove tests for non-existent NumericVa…
Oct 22, 2025
da11283
Fix linker errors - add Shared.vcxitems import to test project so Inp…
Oct 22, 2025
060bacd
Apply clang-format to all SDL compliance files
Oct 22, 2025
f1b8910
lint fix.
Oct 22, 2025
7f34913
Fix test project - add InputValidation files directly instead of impo…
Oct 23, 2025
07aeb68
Fix InputValidation.cpp compilation - disable precompiled headers
Oct 23, 2025
40f999c
Fix C4100 unreferenced parameter warning
Oct 23, 2025
ce5052d
Apply formatting to InputValidation.cpp
Oct 23, 2025
5504ff7
Fix HTTP validation to call callbacks before returning
Oct 23, 2025
dcf5395
Allow localhost for testing - fix RequestOptionsSucceeds crash
Oct 23, 2025
4882ca4
Move URL validation from WinRTHttpResource to HttpModule
Oct 23, 2025
34ddea7
Fix: Allow localhost in HttpModule validation for testing
Oct 23, 2025
cb2d0c1
Fix WebSocket validation same as HTTP
Oct 23, 2025
7cdba13
Fix IPv6 private range validation
Oct 24, 2025
05f4c68
Address Copilot AI review comments
Oct 24, 2025
824fa9e
Fix IPv6 loopback expanded form detection
Oct 24, 2025
29eb242
Fix: Allow localhost in inspector URL validation for Metro packager
Oct 24, 2025
c515a5b
feat: Add DNS validation for advanced SSRF protection
Oct 28, 2025
9c166b7
lint fix
Oct 29, 2025
7342fb2
Address PR review comments: Add centralized allowlists, specific exce…
Nov 3, 2025
4335777
Address remaining 5 PR review comments
Nov 3, 2025
0ec2ef7
Fix OInstance.cpp: Use m_devSettings instead of devSettings parameter
Nov 3, 2025
0578330
Revert OInstance.cpp: Keep devSettings parameter (not m_devSettings)
Nov 3, 2025
f5fd42f
refactor: Apply C++ best practices for SDL compliance
Nov 3, 2025
67bd4d8
test: Update InputValidationTest to use std::exception
Nov 3, 2025
c697ea1
lint fixes.
Nov 3, 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"
}
206 changes: 206 additions & 0 deletions vnext/Microsoft.ReactNative.Cxx.UnitTests/InputValidationTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// 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"}), std::exception);
EXPECT_THROW(URLValidator::ValidateURL("ftp://example.com", {"http", "https"}), std::exception);
EXPECT_THROW(URLValidator::ValidateURL("javascript:alert(1)", {"http", "https"}), std::exception);
}

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

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

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

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"}), std::exception);
}

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

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

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"}), std::exception);
}

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"), std::exception);
EXPECT_THROW(PathValidator::ValidateBlobId("blob/file"), std::exception);
EXPECT_THROW(PathValidator::ValidateBlobId("blob\\file"), std::exception);
}

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), std::exception);
}

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

// ============================================================================
// 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"), std::exception);
}

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"), std::exception);
}

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"), std::exception);
}

// ============================================================================
// 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 std::exception";
} catch (const std::exception &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>
61 changes: 61 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,21 @@ 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 {
if (uri.find("data:") == 0) {
// Validate data URI size to prevent DoS through memory exhaustion
::Microsoft::ReactNative::InputValidation::SizeValidator::ValidateSize(
uri.length(), ::Microsoft::ReactNative::InputValidation::SizeValidator::MAX_DATA_URI_SIZE, "Data URI");
} else {
// Allow http/https only for non-data URIs
::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 +142,21 @@ 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 {
if (uri.find("data:") == 0) {
// Validate data URI size to prevent DoS through memory exhaustion
::Microsoft::ReactNative::InputValidation::SizeValidator::ValidateSize(
uri.length(), ::Microsoft::ReactNative::InputValidation::SizeValidator::MAX_DATA_URI_SIZE, "Data URI");
} else {
// Allow http/https only for non-data URIs
::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 +178,21 @@ 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 {
if (uri.find("data:") == 0) {
// Validate data URI size to prevent DoS through memory exhaustion
::Microsoft::ReactNative::InputValidation::SizeValidator::ValidateSize(
uri.length(), ::Microsoft::ReactNative::InputValidation::SizeValidator::MAX_DATA_URI_SIZE, "Data URI");
} else {
// Allow http/https only for non-data URIs
::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 +202,21 @@ 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 {
if (uri.find("data:") == 0) {
// Validate data URI size to prevent DoS through memory exhaustion
::Microsoft::ReactNative::InputValidation::SizeValidator::ValidateSize(
uri.length(), ::Microsoft::ReactNative::InputValidation::SizeValidator::MAX_DATA_URI_SIZE, "Data URI");
} else {
// Allow http/https only for non-data URIs
::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, ::Microsoft::ReactNative::InputValidation::AllowedSchemes::LINKING_SCHEMES);
} 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"});
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

The allowed schemes list {"http", "https", "mailto", "tel"} is hardcoded here but differs from the LINKING_SCHEMES allowlist defined in InputValidation.h which includes "ms-settings". Use the centralized AllowedSchemes::LINKING_SCHEMES constant for consistency instead of inline literals.

Copilot uses AI. Check for mistakes.
} 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, ::Microsoft::ReactNative::InputValidation::AllowedSchemes::LINKING_SCHEMES);
} 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