From 61ad64963238b524c6fa3d3c14db320eb427be53 Mon Sep 17 00:00:00 2001 From: Jim Studt Date: Thu, 19 Nov 2020 14:38:13 -0600 Subject: [PATCH] Add a 'safeMode' flag to MarkdownParser(). This will prevent prevent HTML on the input from getting to the HTML output. This is useful if you publish user generated content. Notice there were two places we had to recognize safeMode when reading a "<" and choose to use SafedHTML. --- Sources/Ink/API/MarkdownParser.swift | 16 ++++++++++++---- Sources/Ink/API/Modifier.swift | 1 + Sources/Ink/Internal/FormattedText.swift | 2 +- Sources/Ink/Internal/HTML.swift | 18 ++++++++++++++++++ Sources/Ink/Internal/Reader.swift | 6 ++++-- Tests/InkTests/HTMLTests.swift | 15 ++++++++++++++- 6 files changed, 50 insertions(+), 8 deletions(-) diff --git a/Sources/Ink/API/MarkdownParser.swift b/Sources/Ink/API/MarkdownParser.swift index 7445da7..338a50d 100644 --- a/Sources/Ink/API/MarkdownParser.swift +++ b/Sources/Ink/API/MarkdownParser.swift @@ -14,13 +14,21 @@ /// /// To customize how this parser performs its work, attach /// a `Modifier` using the `addModifier` method. +/// +/// To prevent HTML tags from passing through to HTML +/// output for untrusted input, set safeMode:true +/// The angle brackets will be represented as HTML +/// character entities to prevent interpretation. +/// public struct MarkdownParser { private var modifiers: ModifierCollection - + private var safeMode : Bool + /// Initialize an instance, optionally passing an array /// of modifiers used to customize the parsing process. - public init(modifiers: [Modifier] = []) { + public init(modifiers: [Modifier] = [], safeMode: Bool = false) { self.modifiers = ModifierCollection(modifiers: modifiers) + self.safeMode = safeMode } /// Add a modifier to this parser, which can be used to @@ -40,7 +48,7 @@ public struct MarkdownParser { /// both the HTML representation of the given string, and also any /// metadata values found within it. public func parse(_ markdown: String) -> Markdown { - var reader = Reader(string: markdown) + var reader = Reader(string: markdown, safeMode: safeMode) var fragments = [ParsedFragment]() var urlsByName = [String : URL]() var titleHeading: Heading? @@ -132,7 +140,7 @@ private extension MarkdownParser { switch character { case "#": return Heading.self case "!": return Image.self - case "<": return HTML.self + case "<": return safeMode ? SafedHTML.self : HTML.self case ">": return Blockquote.self case "`": return CodeBlock.self case "-" where character == nextCharacter, diff --git a/Sources/Ink/API/Modifier.swift b/Sources/Ink/API/Modifier.swift index 980ecb1..177cbac 100644 --- a/Sources/Ink/API/Modifier.swift +++ b/Sources/Ink/API/Modifier.swift @@ -47,6 +47,7 @@ public extension Modifier { case headings case horizontalLines case html + case safedHtml case images case inlineCode case links diff --git a/Sources/Ink/Internal/FormattedText.swift b/Sources/Ink/Internal/FormattedText.swift index a1d54f8..02d09de 100644 --- a/Sources/Ink/Internal/FormattedText.swift +++ b/Sources/Ink/Internal/FormattedText.swift @@ -333,7 +333,7 @@ private extension FormattedText { case "`": return InlineCode.self case "[": return Link.self case "!": return Image.self - case "<": return HTML.self + case "<": return reader.safeMode ? SafedHTML.self : HTML.self default: return nil } } diff --git a/Sources/Ink/Internal/HTML.swift b/Sources/Ink/Internal/HTML.swift index 53004c6..027eff0 100644 --- a/Sources/Ink/Internal/HTML.swift +++ b/Sources/Ink/Internal/HTML.swift @@ -63,6 +63,24 @@ internal struct HTML: Fragment { } } +internal struct SafedHTML : Fragment { + private var element: Reader.HTMLElement + + static func read(using reader: inout Reader) throws -> SafedHTML { + return try SafedHTML( element: reader.readHTMLElement()) + } + + var modifierTarget: Modifier.Target { .safedHtml } + + func html(usingURLs urls: NamedURLCollection, modifiers: ModifierCollection) -> String { + return "<\(element.name)\(element.isSelfClosing ? "/":"")>" + } + + func plainText() -> String { + return "<\(element.name)\(element.isSelfClosing ? "/":"")>" + } +} + private extension Reader { typealias HTMLElement = (name: Substring, isSelfClosing: Bool) diff --git a/Sources/Ink/Internal/Reader.swift b/Sources/Ink/Internal/Reader.swift index ef597d2..76e5e9e 100644 --- a/Sources/Ink/Internal/Reader.swift +++ b/Sources/Ink/Internal/Reader.swift @@ -7,10 +7,12 @@ internal struct Reader { private let string: String private(set) var currentIndex: String.Index - - init(string: String) { + let safeMode : Bool + + init(string: String, safeMode: Bool = false) { self.string = string self.currentIndex = string.startIndex + self.safeMode = safeMode } } diff --git a/Tests/InkTests/HTMLTests.swift b/Tests/InkTests/HTMLTests.swift index 01f4733..a0e81c9 100644 --- a/Tests/InkTests/HTMLTests.swift +++ b/Tests/InkTests/HTMLTests.swift @@ -111,6 +111,17 @@ final class HTMLTests: XCTestCase { XCTAssertEqual(html, "

Hello & welcome to <Ink>

") } + + func testHTMLSafeMode() { + let html = MarkdownParser(safeMode:true).html(from: "Hello

World

.
Be safe.") + XCTAssertEqual(html, "

Hello<h2>World</h2>.<br/>Be safe.

") + } + + func testHTMLSafeModeFirst() { + let html = MarkdownParser(safeMode:true).html(from: "

Hello


World.") + XCTAssertEqual(html, "<h2>

Hello</h2><br/>World.

") + } + } extension HTMLTests { @@ -127,7 +138,9 @@ extension HTMLTests { ("testInlineSelfClosingHTMLElement", testInlineSelfClosingHTMLElement), ("testTopLevelHTMLLineBreak", testTopLevelHTMLLineBreak), ("testHTMLComment", testHTMLComment), - ("testHTMLEntities", testHTMLEntities) + ("testHTMLEntities", testHTMLEntities), + ("testHTMLSafeMode", testHTMLSafeMode), + ("testHTMLSafeModeFirst", testHTMLSafeModeFirst) ] } }