Skip to content
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

#2414 Show links to remote images #2422

Merged
merged 21 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions Core/source/mobile-interface/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ export class Endpoints {
return fmtRes({}, encrypted);
};

public sanitizeHtml = async (uncheckedReq: unknown): Promise<EndpointRes> => {
const { html } = ValidateInput.sanitizeHtml(uncheckedReq);
const sanitizedHtml = Xss.htmlSanitizeKeepBasicTags(html);
return fmtRes({ sanitizedHtml });
};

public parseDecryptMsg = async (uncheckedReq: unknown, data: Buffers): Promise<EndpointRes> => {
const { keys: kisWithPp, msgPwd, isMime, verificationPubkeys } = ValidateInput.parseDecryptMsg(uncheckedReq);
const rawBlocks: MsgBlock[] = []; // contains parsed, unprocessed / possibly encrypted data
Expand Down
10 changes: 10 additions & 0 deletions Core/source/mobile-interface/validate-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export namespace NodeRequest {
export type composeEmail = ComposeEmailPlain | ComposeEmailEncrypted;
export type encryptMsg = { pubKeys: string[]; msgPwd?: string };
export type encryptFile = { pubKeys: string[]; name: string };
export type sanitizeHtml = {
html: string;
};
export type parseDecryptMsg = {
keys: PrvKeyInfo[];
msgPwd?: string;
Expand Down Expand Up @@ -154,6 +157,13 @@ export class ValidateInput {
throw new Error('Wrong request structure for NodeRequest.parseDecryptMsg');
};

public static sanitizeHtml = (v: unknown): NodeRequest.sanitizeHtml => {
if (isObj(v) && hasProp(v, 'html', 'string')) {
return v as NodeRequest.sanitizeHtml;
}
throw new Error('Wrong request structure for NodeRequest.sanitizeHtml');
};

public static encryptFile = (v: unknown): NodeRequest.encryptFile => {
if (isObj(v) && hasProp(v, 'pubKeys', 'string[]') && hasProp(v, 'name', 'string')) {
return v as NodeRequest.encryptFile;
Expand Down
15 changes: 14 additions & 1 deletion Core/source/platform/xss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ declare const dereq_sanitize_html: (
transformTags?: { [tagName: string]: string | Transformer };
allowedAttributes?: { [tag: string]: string[] };
allowedSchemes?: string[];
allowedStyles?: { [tag: string]: Record<string, string | RegExp[]> };
},
) => string;

Expand Down Expand Up @@ -69,17 +70,28 @@ export class Xss {

// eslint-disable-next-line @typescript-eslint/naming-convention
private static ALLOWED_ATTRS = {
// eslint-disable-next-line @typescript-eslint/naming-convention
'*': ['style'],
a: ['href', 'name', 'target'],
img: ['src', 'width', 'height', 'alt'],
font: ['size', 'color', 'face'],
span: ['color'],
div: ['color'],
p: ['color'],
em: ['style'], // tests rely on this, could potentially remove
td: ['width', 'height'],
hr: ['color', 'height'],
};

// eslint-disable-next-line @typescript-eslint/naming-convention
private static ALLOWED_STYLES = {
// eslint-disable-next-line @typescript-eslint/naming-convention
'*': {
// Existing rules...
// Exclude URLs in background property
background: [/^(?!.*url).+$/],
},
};

// eslint-disable-next-line @typescript-eslint/naming-convention
private static ALLOWED_SCHEMES = ['data', 'http', 'https', 'mailto'];

Expand Down Expand Up @@ -139,6 +151,7 @@ export class Xss {
allowedTags: Xss.ALLOWED_BASIC_TAGS,
allowedAttributes: Xss.ALLOWED_ATTRS,
allowedSchemes: Xss.ALLOWED_SCHEMES,
allowedStyles: Xss.ALLOWED_STYLES,
});
}
cleanHtml = cleanHtml.replace(
Expand Down
8 changes: 8 additions & 0 deletions FlowCrypt.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@
95473C1B297E61DE006C8957 /* SequenceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95473C1A297E61DE006C8957 /* SequenceExtension.swift */; };
9547EF212A5F106E00A048FF /* PassPhraseAlertNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9547EF202A5F106E00A048FF /* PassPhraseAlertNode.swift */; };
9547EF242A5FBA2B00A048FF /* MenuSeparatorCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9547EF232A5FBA2B00A048FF /* MenuSeparatorCellNode.swift */; };
955475FB2B0650AC00F52076 /* WebNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955475FA2B0650AC00F52076 /* WebNode.swift */; };
955475FD2B06512800F52076 /* ThreadDetailWebNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955475FC2B06512800F52076 /* ThreadDetailWebNode.swift */; };
9577CEDD2AA7A4A40084AC62 /* PublicKeyDetailNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9577CEDC2AA7A4A40084AC62 /* PublicKeyDetailNode.swift */; };
9582BC5A2A782DA700439728 /* pass_phrase_hint.html in Resources */ = {isa = PBXBuildFile; fileRef = 9582BC592A782DA700439728 /* pass_phrase_hint.html */; };
958566B72A6126DE001C84D3 /* EncryptedStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F92EE71236F165E009BE0D7 /* EncryptedStorage.swift */; };
Expand Down Expand Up @@ -635,6 +637,8 @@
95473C1A297E61DE006C8957 /* SequenceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceExtension.swift; sourceTree = "<group>"; };
9547EF202A5F106E00A048FF /* PassPhraseAlertNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPhraseAlertNode.swift; sourceTree = "<group>"; };
9547EF232A5FBA2B00A048FF /* MenuSeparatorCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSeparatorCellNode.swift; sourceTree = "<group>"; };
955475FA2B0650AC00F52076 /* WebNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebNode.swift; sourceTree = "<group>"; };
955475FC2B06512800F52076 /* ThreadDetailWebNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDetailWebNode.swift; sourceTree = "<group>"; };
9577CEDC2AA7A4A40084AC62 /* PublicKeyDetailNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicKeyDetailNode.swift; sourceTree = "<group>"; };
9582BC592A782DA700439728 /* pass_phrase_hint.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = pass_phrase_hint.html; sourceTree = "<group>"; };
958566B82A612822001C84D3 /* ASButtonNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASButtonNode.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2223,6 +2227,7 @@
75DCE69F2869EBC0003435F1 /* EmptyFolderCellNode.swift */,
9547EF232A5FBA2B00A048FF /* MenuSeparatorCellNode.swift */,
950F27052AB833EA0041595D /* ContactAddNode.swift */,
955475FC2B06512800F52076 /* ThreadDetailWebNode.swift */,
);
path = "Cell Nodes";
sourceTree = "<group>";
Expand Down Expand Up @@ -2252,6 +2257,7 @@
9547EF202A5F106E00A048FF /* PassPhraseAlertNode.swift */,
95E014CE2A8BF27C00D4B4F5 /* AvatarCheckboxNode.swift */,
9577CEDC2AA7A4A40084AC62 /* PublicKeyDetailNode.swift */,
955475FA2B0650AC00F52076 /* WebNode.swift */,
);
path = Nodes;
sourceTree = "<group>";
Expand Down Expand Up @@ -2871,6 +2877,7 @@
D2CDC3D72404704D002B045F /* RecipientEmailsCellNode.swift in Sources */,
5165ABCC27B526D100CCC379 /* RecipientEmailTextFieldNode.swift in Sources */,
51C56BE82901867D00610D12 /* ENSideMenu.swift in Sources */,
955475FB2B0650AC00F52076 /* WebNode.swift in Sources */,
D2717752242567EB00BDA9A9 /* KeyTextCellNode.swift in Sources */,
511D07E12769FBBA0050417B /* MessageActionCellNode.swift in Sources */,
D211CE7B23FC59ED00D1CE38 /* InfoCellNode.swift in Sources */,
Expand Down Expand Up @@ -2906,6 +2913,7 @@
D24ABA6023FDB26C002EE9DD /* Helpers.swift in Sources */,
9577CEDD2AA7A4A40084AC62 /* PublicKeyDetailNode.swift in Sources */,
D211CE7323FC35AC00D1CE38 /* TextViewCellNode.swift in Sources */,
955475FD2B06512800F52076 /* ThreadDetailWebNode.swift in Sources */,
D2A9CA392426197400E1D898 /* SigninButtonNode.swift in Sources */,
D211CE7423FC35AC00D1CE38 /* ButtonCellNode.swift in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource {
else { return self.dividerNode(indexPath: indexPath) }

guard row > 1 else {
if processedMessage.text.isHTMLString() {
return ThreadDetailWebNode(input: .init(message: processedMessage.text, index: messageIndex))
}
return MessageTextSubjectNode(
input: .init(
message: processedMessage.attributedMessage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ extension ThreadDetailsViewController {
sender: input.rawMessage.sender,
subject: [quoteType.subjectPrefix, subject].joined(),
sentDate: input.rawMessage.date,
text: processedMessage.fullText,
text: processedMessage.fullText.removingHtmlTags() ?? "",
threadId: threadId,
replyToMsgId: replyToMsgId,
inReplyTo: input.rawMessage.inReplyTo,
Expand Down
18 changes: 18 additions & 0 deletions FlowCrypt/Core/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,24 @@ class Core: KeyDecrypter, KeyParser, CoreComposeMessageType {
).data
}

func sanitizeHtml(html: String) async throws -> String {
struct SanitizeHtmlRaw: Decodable {
let sanitizedHtml: String
}
let params: [String: String] = [
"html": html
]

let parsed = try await call(
"sanitizeHtml",
params: params
)

let res = try parsed.json.decodeJson(as: SanitizeHtmlRaw.self)

return res.sanitizedHtml
}

func parseDecryptMsg(
encrypted: Data,
keys: [Keypair],
Expand Down
4 changes: 4 additions & 0 deletions FlowCrypt/Extensions/UIColorExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public extension UIColor {
static var main: UIColor {
UIColor(r: 36, g: 156, b: 6, alpha: 1)
}

static var plainTextBorder: UIColor {
UIColor(hex: "777777") ?? .systemGray4
}

static var textColor: UIColor {
colorFor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ final class MessageHelper {
messageId: message.identifier
)
guard message.isPgp else {
return ProcessedMessage(message: message, keyDetails: keyDetails)
return try await ProcessedMessage(message: message, keyDetails: keyDetails)
}

return try await decryptAndProcess(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright © 2017-present FlowCrypt a. s. All rights reserved.
//

import FlowCryptCommon
import UIKit

struct ProcessedMessage {
Expand Down Expand Up @@ -106,10 +107,15 @@ extension ProcessedMessage {
self.signature = signature
}

init(message: Message, keyDetails: [KeyDetails] = []) {
init(message: Message, keyDetails: [KeyDetails] = []) async throws {
self.message = message
(self.text, self.quote) = Self.parseQuote(text: message.body.text)
self.type = .plain
if let html = message.body.html {
self.text = try await Core.shared.sanitizeHtml(html: html)
self.quote = nil
} else {
(self.text, self.quote) = Self.parseQuote(text: message.body.text)
}
self.attachments = message.attachments
self.signature = .unsigned
self.keyDetails = keyDetails
Expand Down
2 changes: 1 addition & 1 deletion FlowCrypt/Resources/generated/flowcrypt-ios-prod.js.txt

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions FlowCryptCommon/Extensions/StringExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//

import Foundation
import UIKit

public extension String {
var hasContent: Bool {
Expand Down
2 changes: 1 addition & 1 deletion FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public final class MessageTextSubjectNode: CellNode {
if let quote = input.quote {
setupTextNode(quoteNode, text: quote, accessibilityIdentifier: "aid-message-\(input.index)-quote")
}
addLeftBorder(width: .threadLeftBorderWidth, color: input.isEncrypted ? .main : UIColor(hex: "777777"))
addLeftBorder(width: .threadLeftBorderWidth, color: input.isEncrypted ? .main : .plainTextBorder)
}

private func setupTextNode(_ node: ASEditableTextNode, text: NSAttributedString?, accessibilityIdentifier: String) {
Expand Down
68 changes: 68 additions & 0 deletions FlowCryptUI/Cell Nodes/ThreadDetailWebNode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// ThreadDetailWebNode.swift
// FlowCryptUI
//
// Created by Ioan Moldoan on 11/16/23
// Copyright © 2017-present FlowCrypt a. s. All rights reserved.
//

import AsyncDisplayKit

public final class ThreadDetailWebNode: CellNode {
public struct Input {
let message: String?
let index: Int

public init(message: String?, index: Int) {
self.message = message
self.index = index
}
}

private let input: ThreadDetailWebNode.Input

private lazy var webViewNode: CustomWebViewNode = {
let node = CustomWebViewNode()
node.setAccessibilityIdentifier(accessibilityIdentifier: "aid-message-\(input.index)")
node.setHtml("""
<header>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<style>
* { font-family: -apple-system, "Helvetica Neue", sans-serif; }
:root { color-scheme: light dark; supported-color-schemes: light dark; }
@media (prefers-color-scheme: dark) {
:root {
background-color: #2D2C2E;
color: white;
}
a {
color: #1783FD;
}
}
html, body { padding: 0 !important; margin: 0 !important; }
</style>
</header>
\(input.message ?? "")
""")
node.style.flexGrow = 1.0
return node
}()

public init(input: ThreadDetailWebNode.Input) {
self.input = input

super.init()
addLeftBorder(width: .threadLeftBorderWidth, color: .plainTextBorder)
}

override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec {
let specChild: ASLayoutElement

specChild = webViewNode

return ASInsetLayoutSpec(
insets: .threadMessageInsets,
child: specChild
)
}
}
Loading
Loading