Skip to content

Commit 2b4e1f5

Browse files
cipolleschifacebook-github-bot
authored andcommitted
Fix escaping in the URL conversion (facebook#36898)
Summary: Pull Request resolved: facebook#36898 This changes are a followup from D42281798 and the task T141309497. In the previous diff, we were able to handle most cases thanks to `NSURLComponents`. `NSURLComponents` provides us with more flexibility so that we could handle the missing cases. ## Changelog: [iOS][Fixed] - Handle doulbe `#` and partially escaped urls Reviewed By: sammy-SC Differential Revision: D44958172 fbshipit-source-id: 03628d86966c149d0785ad90fdbccbcb5e70106e
1 parent ee2f488 commit 2b4e1f5

File tree

2 files changed

+65
-23
lines changed

2 files changed

+65
-23
lines changed

packages/react-native/React/Base/RCTConvert.m

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -83,30 +83,12 @@ + (NSURL *)NSURL:(id)json
8383
return nil;
8484
}
8585

86-
@try { // NSURL has a history of crashing with bad input, so let's be safe
87-
88-
NSURL *URL = [NSURL URLWithString:path];
89-
if (URL.scheme) { // Was a well-formed absolute URL
90-
return URL;
86+
@try { // NSURL has a history of crashing with bad input, so let's be
87+
NSURLComponents *urlComponents = [NSURLComponents componentsWithString:path];
88+
if (urlComponents.scheme) {
89+
return [self _preprocessURLComponents:urlComponents from:path].URL;
9190
}
9291

93-
// Check if it has a scheme
94-
if ([path rangeOfString:@"://"].location != NSNotFound) {
95-
NSMutableCharacterSet *urlAllowedCharacterSet = [NSMutableCharacterSet new];
96-
[urlAllowedCharacterSet formUnionWithCharacterSet:[NSCharacterSet URLUserAllowedCharacterSet]];
97-
[urlAllowedCharacterSet formUnionWithCharacterSet:[NSCharacterSet URLPasswordAllowedCharacterSet]];
98-
[urlAllowedCharacterSet formUnionWithCharacterSet:[NSCharacterSet URLHostAllowedCharacterSet]];
99-
[urlAllowedCharacterSet formUnionWithCharacterSet:[NSCharacterSet URLPathAllowedCharacterSet]];
100-
[urlAllowedCharacterSet formUnionWithCharacterSet:[NSCharacterSet URLQueryAllowedCharacterSet]];
101-
[urlAllowedCharacterSet formUnionWithCharacterSet:[NSCharacterSet URLFragmentAllowedCharacterSet]];
102-
path = [path stringByAddingPercentEncodingWithAllowedCharacters:urlAllowedCharacterSet];
103-
URL = [NSURL URLWithString:path];
104-
if (URL) {
105-
return URL;
106-
}
107-
}
108-
109-
// Assume that it's a local path
11092
path = path.stringByRemovingPercentEncoding;
11193
if ([path hasPrefix:@"~"]) {
11294
// Path is inside user directory
@@ -115,7 +97,8 @@ + (NSURL *)NSURL:(id)json
11597
// Assume it's a resource path
11698
path = [[NSBundle mainBundle].resourcePath stringByAppendingPathComponent:path];
11799
}
118-
if (!(URL = [NSURL fileURLWithPath:path])) {
100+
NSURL *URL = [NSURL fileURLWithPath:path];
101+
if (!URL) {
119102
RCTLogConvertError(json, @"a valid URL");
120103
}
121104
return URL;
@@ -125,6 +108,32 @@ + (NSURL *)NSURL:(id)json
125108
}
126109
}
127110

111+
// This function preprocess the URLComponents received to make sure that we decode it properly
112+
// handling all the use cases.
113+
// See the `RCTConvert_NSURLTests` file for a list of use cases that we want to support:
114+
// To achieve that, we are currently splitting the url, extracting the fragment, so we can
115+
// decode and encode everything but the fragment (which has to be left unmodified)
116+
+ (NSURLComponents *)_preprocessURLComponents:(NSURLComponents *)urlComponents from:(NSString *)path
117+
{
118+
// https://developer.apple.com/documentation/foundation/nsurlcomponents
119+
// "[NSURLComponents's] behavior differs subtly from the NSURL class, which conforms to older RFCs"
120+
// Specifically, NSURL rejects some URLs that NSURLComponents will handle
121+
// gracefully.
122+
NSRange fragmentRange = urlComponents.rangeOfFragment;
123+
if (fragmentRange.length == 0) {
124+
// No fragment, pre-remove all escaped characters so we can encode them once.
125+
return [NSURLComponents componentsWithString:path.stringByRemovingPercentEncoding];
126+
}
127+
// Pre-remove all escaped characters (excluding the fragment) to handle partially encoded strings
128+
NSString *baseUrlString = [path substringToIndex:fragmentRange.location].stringByRemovingPercentEncoding;
129+
// Fragment must be kept as they are passed. We don't have to escape them
130+
NSString *unmodifiedFragment = [path substringFromIndex:fragmentRange.location];
131+
132+
// Recreate the url by using a decoded base and an unmodified fragment.
133+
NSString *preprocessedURL = [NSString stringWithFormat:@"%@%@", baseUrlString, unmodifiedFragment];
134+
return [NSURLComponents componentsWithString:preprocessedURL];
135+
}
136+
128137
RCT_ENUM_CONVERTER(
129138
NSURLRequestCachePolicy,
130139
(@{

packages/rn-tester/RNTesterUnitTests/RCTConvert_NSURLTests.m

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,37 @@ - (void)testDataURL
7777
XCTAssertEqualObjects([testURL absoluteString], [expectedURL absoluteString]);
7878
}
7979

80+
// Escaping edge cases
81+
TEST_URL(
82+
urlWithMultipleHashes,
83+
@"https://example.com/#/abc/#test:example.com",
84+
@"https://example.com/#/abc/%23test:example.com")
85+
TEST_URL(urlWithEqualsInQuery, @"https://example.com/abc.def?ghi=1234", @"https://example.com/abc.def?ghi=1234")
86+
TEST_URL(
87+
urlWithEscapedCharacterInFragment,
88+
@"https://example.com/abc/def.ghi#jkl-mno%27p-qrs",
89+
@"https://example.com/abc/def.ghi#jkl-mno%27p-qrs")
90+
TEST_URL(
91+
urlWithLongQuery,
92+
@"https://example.com/abc?q=def+ghi+jkl&mno=p-q-r-s&tuv=wxy&z_=abc&abc=5",
93+
@"https://example.com/abc?q=def+ghi+jkl&mno=p-q-r-s&tuv=wxy&z_=abc&abc=5")
94+
TEST_URL(
95+
urlWithEscapedCharacterInPathFragment,
96+
@"https://example.com/#/abc/%23def%3Aghi.org",
97+
@"https://example.com/#/abc/%23def%3Aghi.org")
98+
TEST_URL(
99+
urlWithEscapedCharacterInQuery,
100+
@"https://site.com/script?foo=bar#this_ref",
101+
@"https://site.com/script?foo=bar#this_ref")
102+
TEST_URL(
103+
urlWithUnescapedJson,
104+
@"https://example.com/?{\"key\":\"value\"}",
105+
@"https://example.com/?%7B%22key%22:%22value%22%7D")
106+
TEST_URL(
107+
urlWithPartiallyEscapedData,
108+
@"https://example.com/?{%22key%22:%22value%22}",
109+
@"https://example.com/?%7B%22key%22:%22value%22%7D")
110+
// NOTE: This is illegal per RFC 3986, but earlier URL specs allowed it
111+
TEST_URL(urlWithSquareBracketInPath, @"http://www.foo.com/file[.html", @"http://www.foo.com/file%5B.html")
112+
80113
@end

0 commit comments

Comments
 (0)