From d23ee0961a6d20eb2ae367ce68e2caf3105fd81b Mon Sep 17 00:00:00 2001 From: martgil <46025304+martgil@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:09:10 +0800 Subject: [PATCH 1/9] feat: secure email rendering on thread view --- .../Threads/ThreadDetailsViewController.swift | 4 +++- .../Message Provider/MessageHelper.swift | 2 +- FlowCryptUI/Cell Nodes/ThreadDetailWebNode.swift | 1 + FlowCryptUI/Nodes/WebNode.swift | 12 ++++++++++-- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 923ee0256..10af07139 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -266,9 +266,11 @@ final class ThreadDetailsViewController: TableNodeViewController { isUsingKeyManager: appContext.clientConfigurationProvider.configuration.isUsingKeyManager ) + let sanitizedText = try await Core.shared.sanitizeHtml(html: decryptedText) + let processedMessage = ProcessedMessage( message: data.rawMessage, - text: decryptedText, + text: sanitizedText, type: .plain ) handle(processedMessage: processedMessage, at: indexPath) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift index e6092cc54..2f9b9e0ac 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift @@ -260,7 +260,7 @@ final class MessageHelper { signature = nil } else { // decrypt / process success - text = decrypted.text + text = try await Core.shared.sanitizeHtml(html: decrypted.text) messageType = decrypted.replyType == ReplyType.encrypted ? .encrypted : .plain signature = await evaluateSignatureVerificationResult( signature: decrypted.blocks.first?.verifyRes diff --git a/FlowCryptUI/Cell Nodes/ThreadDetailWebNode.swift b/FlowCryptUI/Cell Nodes/ThreadDetailWebNode.swift index 0b56d31fe..f626810f1 100644 --- a/FlowCryptUI/Cell Nodes/ThreadDetailWebNode.swift +++ b/FlowCryptUI/Cell Nodes/ThreadDetailWebNode.swift @@ -60,6 +60,7 @@ public final class ThreadDetailWebNode: CellNode { return """
+ -
+ \(html ?? "") """ // swiftlint:enable line_length From dca529f9269b81c8371fd63a89ce8452cc14bf5a Mon Sep 17 00:00:00 2001 From: martgil <46025304+martgil@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:31:28 +0800 Subject: [PATCH 8/9] fix: block quotes issue --- .../Threads/ThreadDetailsViewController.swift | 4 +++- .../Mail Provider/Message Provider/MessageHelper.swift | 4 +++- .../Message Provider/ProcessedMessage.swift | 10 +++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 10af07139..c646d3809 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -266,7 +266,9 @@ final class ThreadDetailsViewController: TableNodeViewController { isUsingKeyManager: appContext.clientConfigurationProvider.configuration.isUsingKeyManager ) - let sanitizedText = try await Core.shared.sanitizeHtml(html: decryptedText) + let sanitizedText = try await (Core.shared.sanitizeHtml(html: decryptedText)) + .replacingOccurrences(of: ">", with: ">") + .replacingOccurrences(of: "<", with: "<") let processedMessage = ProcessedMessage( message: data.rawMessage, diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift index 2f9b9e0ac..69fa99820 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift @@ -260,7 +260,9 @@ final class MessageHelper { signature = nil } else { // decrypt / process success - text = try await Core.shared.sanitizeHtml(html: decrypted.text) + text = try await (Core.shared.sanitizeHtml(html: decrypted.text)) + .replacingOccurrences(of: ">", with: ">") + .replacingOccurrences(of: "<", with: "<") messageType = decrypted.replyType == ReplyType.encrypted ? .encrypted : .plain signature = await evaluateSignatureVerificationResult( signature: decrypted.blocks.first?.verifyRes diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/ProcessedMessage.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/ProcessedMessage.swift index eff0679fd..9f6c1a65c 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/ProcessedMessage.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/ProcessedMessage.swift @@ -113,10 +113,14 @@ extension ProcessedMessage { self.type = .plain if let html = message.body.html { let (text, quote) = Self.parseHtmlQuote(from: html) - self.text = try await Core.shared.sanitizeHtml(html: text) + self.text = try await (Core.shared.sanitizeHtml(html: text)) + .replacingOccurrences(of: ">", with: ">") + .replacingOccurrences(of: "<", with: "<") if let quote { - // SanitizeHtml replaces > with > so need to convert it back - self.quote = try await (Core.shared.sanitizeHtml(html: quote)).replacingOccurrences(of: ">", with: ">") + // SanitizeHtml replaces > with > and < with < so need to convert them back + self.quote = try await (Core.shared.sanitizeHtml(html: quote)) + .replacingOccurrences(of: ">", with: ">") + .replacingOccurrences(of: "<", with: "<") } else { self.quote = nil } From fe729057da81ae47850c5e75d936eb8f0db429c4 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 16 Jul 2026 15:48:33 +0300 Subject: [PATCH 9/9] fix quotes parsing --- FlowCrypt.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/swiftpm/Package.resolved | 12 +- .../Threads/ThreadDetailsViewController.swift | 8 +- FlowCrypt/Core/Core.swift | 6 +- .../Message Provider/MessageHelper.swift | 53 ++++--- .../Message Provider/ProcessedMessage.swift | 21 +-- .../Core/HTMLSanitizationTests.swift | 141 ++++++++++++++++++ 7 files changed, 205 insertions(+), 44 deletions(-) diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index a31d508dc..07c2b4f81 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -271,7 +271,6 @@ 9F97657D267E18FE0058419D /* client_configuraion_empty.json in Resources */ = {isa = PBXBuildFile; fileRef = 21EA3B3526565B8100691848 /* client_configuraion_empty.json */; }; 9F976584267E194F0058419D /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DAD60722E4588800F2C4CD /* TestData.swift */; }; 9F976585267E194F0058419D /* FlowCryptCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DAD5FD22E4574B00F2C4CD /* FlowCryptCoreTests.swift */; }; - AEC30B9A2DAF433A8783F432 /* HTMLSanitizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDC7CECAFB84E949FB2DAC2 /* HTMLSanitizationTests.swift */; }; 9F9AAFFD2383E216000A00F1 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9AAFFC2383E216000A00F1 /* Document.swift */; }; 9F9ABC8723AC1EAA00D560E3 /* MessageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9ABC8623AC1EAA00D560E3 /* MessageContext.swift */; }; 9FA19890253C841F008C9CF2 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA1988F253C841F008C9CF2 /* TableViewController.swift */; }; @@ -318,6 +317,7 @@ A34D222A27294C67004E0220 /* PubLookupTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34D222827294C67004E0220 /* PubLookupTest.swift */; }; A36108E9273C7A2E00A90E34 /* MockError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36108E8273C7A2E00A90E34 /* MockError.swift */; }; A3B7C31923F576BA0022D628 /* AppStartup.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B7C31823F576BA0022D628 /* AppStartup.swift */; }; + AEC30B9A2DAF433A8783F432 /* HTMLSanitizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDC7CECAFB84E949FB2DAC2 /* HTMLSanitizationTests.swift */; }; C132B9B41EC2DBD800763715 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C132B9B31EC2DBD800763715 /* AppDelegate.swift */; }; C132B9BB1EC2DBD800763715 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C132B9BA1EC2DBD800763715 /* Assets.xcassets */; }; C132B9BE1EC2DBD800763715 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C132B9BC1EC2DBD800763715 /* LaunchScreen.storyboard */; }; @@ -567,6 +567,7 @@ 32DCAC9C0512037018F434A1 /* BackendApi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackendApi.swift; sourceTree = ""; }; 32DCAEFF16F5D91A35791730 /* DataExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataExtensions.swift; sourceTree = ""; }; 32DCAF8424D0185FAA9401A7 /* Imap+send.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Imap+send.swift"; sourceTree = ""; }; + 3BDC7CECAFB84E949FB2DAC2 /* HTMLSanitizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLSanitizationTests.swift; sourceTree = ""; }; 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentNode.swift; sourceTree = ""; }; 5109A77B272153B400D2CEB9 /* LeftAlignedCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftAlignedCollectionViewFlowLayout.swift; sourceTree = ""; }; 510BB63427BE92CC00B1011F /* RecipientBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientBase.swift; sourceTree = ""; }; @@ -817,7 +818,6 @@ A3B7C31623E437370022D628 /* FlowCryptRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FlowCryptRelease.entitlements; sourceTree = ""; }; A3B7C31823F576BA0022D628 /* AppStartup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStartup.swift; sourceTree = ""; }; A3DAD5FD22E4574B00F2C4CD /* FlowCryptCoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowCryptCoreTests.swift; sourceTree = ""; }; -3BDC7CECAFB84E949FB2DAC2 /* HTMLSanitizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLSanitizationTests.swift; sourceTree = ""; }; A3DAD60722E4588800F2C4CD /* TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = ""; }; C132B9B01EC2DBD800763715 /* FlowCrypt.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FlowCrypt.app; sourceTree = BUILT_PRODUCTS_DIR; }; C132B9B31EC2DBD800763715 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -3921,7 +3921,7 @@ repositoryURL = "https://github.com/scinfu/SwiftSoup.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.13.5; + minimumVersion = 2.13.6; }; }; 95D83FB82A5D46C3006FDC33 /* XCRemoteSwiftPackageReference "SwiftLint" */ = { @@ -3929,7 +3929,7 @@ repositoryURL = "https://github.com/realm/SwiftLint"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.63.3; + minimumVersion = 0.65.0; }; }; 95F55F982A7B89260000E50F /* XCRemoteSwiftPackageReference "ProgressHUD" */ = { diff --git a/FlowCrypt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 253dc8394..52711e49b 100644 --- a/FlowCrypt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -177,8 +177,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "51c8c237beea1baa9cac64ef83cec68c6790506c", - "version" : "604.0.0-prerelease-2026-04-21" + "revision" : "a8b1c535647243d603b6772e7e8306c993a0c188", + "version" : "605.0.0-prerelease-2026-06-26" } }, { @@ -186,8 +186,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/SwiftLint", "state" : { - "revision" : "70a5f3225d940d4573d3d2ffcf85b07ab2a6c5de", - "version" : "0.63.3" + "revision" : "fd768ba9a0e8a4f96d550d98de6c4cf2af565cf1", + "version" : "0.65.0" } }, { @@ -195,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scinfu/SwiftSoup.git", "state" : { - "revision" : "49dcadd93161f4a44b4994d3a3e8de9f085aface", - "version" : "2.13.5" + "revision" : "ead56133a693d0184d8c2db1a6d6394410cacfd6", + "version" : "2.13.6" } }, { diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index c646d3809..ee3a5c8da 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -44,7 +44,9 @@ final class ThreadDetailsViewController: TableNodeViewController { } } - var currentFolderPath: String { inboxItem.folderPath } + var currentFolderPath: String { + inboxItem.folderPath + } let onComposeMessageAction: ((ComposeMessageAction) -> Void)? let onComplete: MessageActionCompletion @@ -266,9 +268,7 @@ final class ThreadDetailsViewController: TableNodeViewController { isUsingKeyManager: appContext.clientConfigurationProvider.configuration.isUsingKeyManager ) - let sanitizedText = try await (Core.shared.sanitizeHtml(html: decryptedText)) - .replacingOccurrences(of: ">", with: ">") - .replacingOccurrences(of: "<", with: "<") + let sanitizedText = try await Core.shared.sanitizeHtml(html: decryptedText) let processedMessage = ProcessedMessage( message: data.rawMessage, diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index f3d9f3ee8..3dcdb6b6f 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -262,11 +262,11 @@ class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { return try r.json.decodeJson(as: CoreRes.ZxcvbnStrengthBar.self) } - private func waitUntilJavascirptReady(timeout: TimeInterval = 1) async throws { + func waitUntilJavaScriptReady(timeout: TimeInterval = 1) async throws { let functionName = "handleRequestFromHost" let deadline = Date().addingTimeInterval(timeout) while Date() < deadline { - if await (try? webView.callAsyncJavaScript( + if let webView, await (try? webView.callAsyncJavaScript( "return typeof \(functionName) === 'function';", arguments: [:], contentWorld: .page @@ -285,7 +285,7 @@ class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { let requestData = [UInt8](data) do { - try await waitUntilJavascirptReady() + try await waitUntilJavaScriptReady() let response = try await webView.callAsyncJavaScript( "return handleRequestFromHost(\"\(endpoint)\", \(paramsData), \(requestData))", arguments: [:], diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift index 69fa99820..b54a48531 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift @@ -130,7 +130,9 @@ final class MessageHelper { } private func fetchOrDownloadData(for attachment: MessageAttachment, messageId: Identifier) async throws -> Data { - if let data = attachment.data { return data } + if let data = attachment.data { + return data + } return try await download(attachment: attachment, messageId: messageId, progressHandler: nil) } @@ -232,6 +234,31 @@ final class MessageHelper { private func process( message: Message, with decrypted: CoreRes.ParseDecryptMsg + ) async throws -> ProcessedMessage { + var attachments: [MessageAttachment] = if message.raw != nil || message.attachments.isEmpty { + decrypted.blocks.compactMap(\.attMeta).compactMap(MessageAttachment.init) + } else { + message.attachments + } + + let keyDetails = try await getKeyDetailsFromAttachment( + attachments: &attachments, + messageId: message.identifier + ) + + return try await Self.makeProcessedMessage( + message: message, + decrypted: decrypted, + attachments: attachments, + keyDetails: keyDetails + ) + } + + static func makeProcessedMessage( + message: Message, + decrypted: CoreRes.ParseDecryptMsg, + attachments: [MessageAttachment], + keyDetails: [KeyDetails] ) async throws -> ProcessedMessage { let firstBlockParseErr = decrypted.blocks.first { $0.type == .blockParseErr } let firstDecryptErrBlock = decrypted.blocks.first { $0.type == .decryptErr } @@ -260,27 +287,17 @@ final class MessageHelper { signature = nil } else { // decrypt / process success - text = try await (Core.shared.sanitizeHtml(html: decrypted.text)) - .replacingOccurrences(of: ">", with: ">") - .replacingOccurrences(of: "<", with: "<") + text = try await Core.shared.sanitizeHtml(html: decrypted.text) messageType = decrypted.replyType == ReplyType.encrypted ? .encrypted : .plain - signature = await evaluateSignatureVerificationResult( + signature = evaluateSignatureVerificationResult( signature: decrypted.blocks.first?.verifyRes ) } - var attachments: [MessageAttachment] = if message.raw != nil || message.attachments.isEmpty { - decrypted.blocks.compactMap(\.attMeta).compactMap(MessageAttachment.init) - } else { - message.attachments - } - - let keyDetails: [KeyDetails] = try await getKeyDetailsFromAttachment(attachments: &attachments, messageId: message.identifier) - // Also extract keyDetails from publicKey blocks (for encrypted messages) let publicKeyBlockDetails: [KeyDetails] = decrypted.blocks .filter { $0.type == .publicKey } - .compactMap { $0.keyDetails } + .compactMap(\.keyDetails) return ProcessedMessage( message: message, @@ -345,7 +362,9 @@ extension MessageHelper { guard let sender else { return [] } let pubKeys = try localContactsProvider.retrievePubKeys(for: sender.email, shouldUpdateLastUsed: false).map(\.armored) - if pubKeys.isNotEmpty || onlyLocal { return pubKeys } + if pubKeys.isNotEmpty || onlyLocal { + return pubKeys + } // try? because we may ignore update remote result try? await pubLookup.fetchRemoteUpdateLocal(with: sender) @@ -355,9 +374,9 @@ extension MessageHelper { return contact.pubKeys.map(\.armored) } - private func evaluateSignatureVerificationResult( + private static func evaluateSignatureVerificationResult( signature: MsgBlock.VerifyRes? - ) async -> ProcessedMessage.MessageSignature { + ) -> ProcessedMessage.MessageSignature { guard let signature else { return .unsigned } if let error = signature.error { diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/ProcessedMessage.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/ProcessedMessage.swift index 9f6c1a65c..b1bb15269 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/ProcessedMessage.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/ProcessedMessage.swift @@ -113,14 +113,9 @@ extension ProcessedMessage { self.type = .plain if let html = message.body.html { let (text, quote) = Self.parseHtmlQuote(from: html) - self.text = try await (Core.shared.sanitizeHtml(html: text)) - .replacingOccurrences(of: ">", with: ">") - .replacingOccurrences(of: "<", with: "<") + self.text = try await Core.shared.sanitizeHtml(html: text) if let quote { - // SanitizeHtml replaces > with > and < with < so need to convert them back - self.quote = try await (Core.shared.sanitizeHtml(html: quote)) - .replacingOccurrences(of: ">", with: ">") - .replacingOccurrences(of: "<", with: "<") + self.quote = try await Core.shared.sanitizeHtml(html: quote) } else { self.quote = nil } @@ -205,7 +200,7 @@ extension ProcessedMessage { guard let lastLine = lines.popLast() else { break } let trimmedLine = lastLine.trimmingCharacters(in: .whitespaces) - if trimmedLine.isEmpty || trimmedLine.hasPrefix(">") { + if trimmedLine.isEmpty || trimmedLine.hasPrefix(">") || trimmedLine.hasPrefix(">") { quoteLines.insert(lastLine, at: 0) } else { if trimmedLine.hasPrefix("On "), trimmedLine.hasSuffix(" wrote:") { @@ -227,11 +222,17 @@ extension ProcessedMessage { } var attributedMessage: NSAttributedString { - String(text.prefix(maxLength)).attributed(color: type.textColor) + Self.decodeHTMLEntities(in: String(text.prefix(maxLength))) + .attributed(color: type.textColor) } var attributedQuote: NSAttributedString? { guard let quote else { return nil } - return String(quote.prefix(maxLength)).attributed(color: type.textColor.withAlphaComponent(0.8)) + return Self.decodeHTMLEntities(in: String(quote.prefix(maxLength))) + .attributed(color: type.textColor.withAlphaComponent(0.8)) + } + + private static func decodeHTMLEntities(in text: String) -> String { + (try? Entities.unescape(text)) ?? text } } diff --git a/FlowCryptAppTests/Core/HTMLSanitizationTests.swift b/FlowCryptAppTests/Core/HTMLSanitizationTests.swift index 00d90b855..46286fb1d 100644 --- a/FlowCryptAppTests/Core/HTMLSanitizationTests.swift +++ b/FlowCryptAppTests/Core/HTMLSanitizationTests.swift @@ -11,6 +11,11 @@ import XCTest class HTMLSanitizationTests: XCTestCase { let core: Core = .shared + override func setUp() async throws { + try await super.setUp() + try await core.waitUntilJavaScriptReady(timeout: 10) + } + // MARK: - End-to-end: decrypt a PGP-encrypted XSS payload, then verify sanitization func testDecryptedXssPayloadIsStrippedAfterFullRoundtrip() async throws { @@ -61,6 +66,63 @@ class HTMLSanitizationTests: XCTestCase { ) } + func testDecryptedEntityEncodedXssIsSanitizedByMessageHelper() async throws { + let key = TestData.k0 + let encodedHtml = """ +
FC_ENTITY_ENCODED_XSS_POC
+ <script> + fetch('https://example.com/entity-xss-callback') + </script> + """ + let encodedPayload = """ + MIME-Version: 1.0 + Content-Type: text/html; charset=UTF-8 + + \(encodedHtml) + """ + let encrypted = try await core.encrypt( + data: encodedPayload.data(), + pubKeys: [key.public], + password: nil + ) + let decrypted = try await core.parseDecryptMsg( + encrypted: encrypted, + keys: [key], + msgPwd: nil, + isMime: false, + verificationPubKeys: [] + ) + + XCTAssertTrue( + decrypted.text.contains("