From bfd2a6ea8dec9f52b02771f390431a3431e569e2 Mon Sep 17 00:00:00 2001 From: Ardlan Khalili Date: Fri, 30 Aug 2024 16:24:20 -0700 Subject: [PATCH] fix: sanitize unescaped problematic url regex strings --- .../Action.Execute.With.RegexValidation.json | 85 +++++++++++++++++++ .../AdaptiveCards/ACOAdaptiveCard.mm | 80 ++++++++++++++++- 2 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 samples/v1.4/Tests/Action.Execute.With.RegexValidation.json diff --git a/samples/v1.4/Tests/Action.Execute.With.RegexValidation.json b/samples/v1.4/Tests/Action.Execute.With.RegexValidation.json new file mode 100644 index 000000000..4dbeb4b0a --- /dev/null +++ b/samples/v1.4/Tests/Action.Execute.With.RegexValidation.json @@ -0,0 +1,85 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "TextBlock", + "text": "Present a form and submit it back to the originator (with validation!)" + }, + { + "type": "ActionSet", + "actions": [ + { + "type": "Action.Execute", + "title": "ActionSet Execute", + "verb": "doActionSetStuff", + "iconUrl": "https://adaptivecards.io/content/Closed%20bug%2092x92.png", + "associatedInputs": "none", + "data": { + "y": -1 + } + }, + { + "type": "Action.ShowCard", + "title": "ShowCard", + "card": { + "type": "AdaptiveCard", + "actions": [ + { + "type": "Action.Execute", + "title": "Neat!", + "associatedInputs": "none" + } + ] + } + } + ] + }, + { + "type": "Input.Text", + "id": "firstRegexCase", + "isRequired": true, + "regex": "^([a-zA-Z0-9._%+-]+@outokumpu\.com)(,[a-zA-Z0-9._%+-]+@outokumpu\.com)*$", + "label": "What is your first regex case?" + }, + { + "type": "Input.Text", + "id": "secondRegexCase", + "regex": "^([a-zA-Z0-9._%+-]+@outokumpu\\.com)(,[a-zA-Z0-9._%+-]+@outokumpu\\.com)*$", + "label": "What is your second regex case?" + }, + { + "type": "Input.Text", + "id": "thirdRegexCase", + "isRequired": true, + "regex": "^([a-zA-Z0-9._%+-]+@outokumpu\\\.com)(,[a-zA-Z0-9._%+-]+@outokumpu\\\.com)*$", + "label": "What is your third regex case?" + }, + { + "type": "Input.Text", + "id": "fourthRegexCase", + "isRequired": true, + "regex": "^([a-zA-Z0-9._%+-]+@outokumpu\\\\.com)(,[a-zA-Z0-9._%+-]+@outokumpu\\\\.com)*$", + "label": "What is your fourth regex case?" + }, + { + "type": "Input.Text", + "id": "fifthRegexCase", + "isRequired": true, + "regex": "^([a-zA-Z0-9._%+-]+@outokumpu\\\\\.com)(,[a-zA-Z0-9._%+-]+@outokumpu\\\\\.com)*$", + "label": "What is your fifth regex case?" + } + ], + "actions": [ + { + "type": "Action.Execute", + "title": "Action.Execute", + "verb": "doStuff", + "iconUrl": "https://adaptivecards.io/content/Closed%20bug%2092x92.png", + "data": { + "x": 13 + } + } + ] +} \ No newline at end of file diff --git a/source/ios/AdaptiveCards/AdaptiveCards/AdaptiveCards/ACOAdaptiveCard.mm b/source/ios/AdaptiveCards/AdaptiveCards/AdaptiveCards/ACOAdaptiveCard.mm index f8b69736b..8cf6d8418 100644 --- a/source/ios/AdaptiveCards/AdaptiveCards/AdaptiveCards/ACOAdaptiveCard.mm +++ b/source/ios/AdaptiveCards/AdaptiveCards/AdaptiveCards/ACOAdaptiveCard.mm @@ -56,11 +56,84 @@ - (NSData *)inputs return _inputs; } -+ (ACOAdaptiveCardParseResult *)fromJson:(NSString *)payload; -{ ++ (NSString *)correctInvalidJsonEscapes:(NSString *)payload { + NSError *regexError = nil; + + // Regex pattern to find each "regex": "..." entry with capturing groups for before, regex string, and after + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(\"regex\"\\s*:\\s*\")(.*?)(\")" + options:NSRegularExpressionDotMatchesLineSeparators + error:®exError]; + + if (regexError != nil) { + NSLog(@"Regex Error: %@", regexError.localizedDescription); + return payload; // Return the original payload if regex creation fails + } + + NSMutableString *mutablePayload = [payload mutableCopy]; + + // Enumerate over each regex match and correct backslashes incrementally + __block NSInteger offset = 0; // Track adjustments to the range after replacements + [regex enumerateMatchesInString:payload + options:0 + range:NSMakeRange(0, payload.length) + usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop) { + if (match.numberOfRanges == 4) { // Ensure we have all 4 capturing groups + // Adjust ranges by offset to account for previous replacements + NSRange fullMatchRange = NSMakeRange(match.range.location + offset, match.range.length); + NSRange regexStringRange = NSMakeRange([match rangeAtIndex:2].location + offset, [match rangeAtIndex:2].length); + + // Validate ranges to prevent out-of-bounds errors + if (NSLocationInRange(regexStringRange.location, NSMakeRange(0, mutablePayload.length)) && + NSLocationInRange(NSMaxRange(regexStringRange), NSMakeRange(0, mutablePayload.length))) { + + // Extract the regex string + NSString *regexString = [mutablePayload substringWithRange:regexStringRange]; + + // Correct escaping: Replace each single backslash with two backslashes + NSString *correctedRegexString = [regexString stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; + + // Reconstruct the corrected full regex entry + NSString *correctedFullEntry = [NSString stringWithFormat:@"%@%@%@", [payload substringWithRange:[match rangeAtIndex:1]], correctedRegexString, [payload substringWithRange:[match rangeAtIndex:3]]]; + + // Replace the entire match with the corrected entry + [mutablePayload replaceCharactersInRange:fullMatchRange withString:correctedFullEntry]; + + // Update offset by the difference in length between corrected and original entries + offset += correctedFullEntry.length - fullMatchRange.length; + } + } + }]; + + return [mutablePayload copy]; +} + ++ (BOOL)isValidJson:(NSString *)jsonString error:(NSError **)error { + NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + if (!jsonData) { + return NO; + } + + [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:error]; + return (*error == nil); +} + ++ (ACOAdaptiveCardParseResult *)fromJson:(NSString *)payload { const std::string g_version = "1.6"; ACOAdaptiveCardParseResult *result = nil; + if (payload) { + NSError *jsonError = nil; + + // First, check if the JSON is valid + if (![self isValidJson:payload error:&jsonError]) { + NSLog(@"Initial JSON Deserialization Error: %@", jsonError.localizedDescription); + // Attempt to fix the JSON using regex replacement + NSString *processedPayload = [self correctInvalidJsonEscapes:payload]; + // Update payload to the corrected version + payload = processedPayload; + } + + // Use the already validated JSON object without re-serialization try { ACOAdaptiveCard *card = [[ACOAdaptiveCard alloc] init]; std::shared_ptr parseResult = AdaptiveCard::DeserializeFromString(std::string([payload UTF8String]), g_version); @@ -77,7 +150,7 @@ + (ACOAdaptiveCardParseResult *)fromJson:(NSString *)payload; } result = [[ACOAdaptiveCardParseResult alloc] init:card errors:nil warnings:acrParseWarnings]; } catch (const AdaptiveCardParseException &e) { - // converts AdaptiveCardParseException to NSError + // Converts AdaptiveCardParseException to NSError ErrorStatusCode errorStatusCode = e.GetStatusCode(); NSInteger errorCode = (long)errorStatusCode; NSBundle *adaptiveCardsBundle = [[ACOBundle getInstance] getBundle]; @@ -92,6 +165,7 @@ + (ACOAdaptiveCardParseResult *)fromJson:(NSString *)payload; result = [[ACOAdaptiveCardParseResult alloc] init:nil errors:errors warnings:nil]; } } + return result; }