Skip to content

Commit 22144f5

Browse files
committed
Revise the escaping
1 parent 5ca3b97 commit 22144f5

5 files changed

Lines changed: 137 additions & 34 deletions

File tree

Sources/HTMLKit/Abstraction/Elements/BodyElements.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21138,7 +21138,7 @@ public struct Script: ContentNode, HeadElement, BodyElement, FormElement, Figure
2113821138
///
2113921139
/// - Parameter content: The script's content.
2114021140
public init(@ContentBuilder<String> content: () -> [String]) {
21141-
self.content = [HtmlString(content())]
21141+
self.content = [JsString(content())]
2114221142
}
2114321143

2114421144
internal init(attributes: OrderedDictionary<String, Any>?, content: [Content]) {

Sources/HTMLKit/Abstraction/Elements/HeadElements.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,7 @@ public struct Style: ContentNode, HeadElement {
715715
///
716716
/// - Parameter content: The style's content.
717717
public init(@ContentBuilder<String> content: () -> [String]) {
718-
self.content = [HtmlString(content())]
718+
self.content = [CssString(content())]
719719
}
720720

721721
internal init(attributes: OrderedDictionary<String, Any>?, content: [Content]) {

Sources/HTMLKit/Framework/Rendering/Encoding/HtmlString.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,29 @@ public struct HtmlString: Content {
1818
self.value = values.joined()
1919
}
2020
}
21+
22+
internal struct JsString: Content {
23+
24+
internal let value: String
25+
26+
internal init(_ value: String) {
27+
self.value = value
28+
}
29+
30+
internal init(_ values: [String]) {
31+
self.value = values.joined()
32+
}
33+
}
34+
35+
internal struct CssString: Content {
36+
37+
internal let value: String
38+
39+
internal init(_ value: String) {
40+
self.value = value
41+
}
42+
43+
internal init(_ values: [String]) {
44+
self.value = values.joined()
45+
}
46+
}

Sources/HTMLKit/Framework/Rendering/Renderer.swift

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,29 @@ public struct Renderer {
4343
}
4444
}
4545

46+
/// A enumeration of potential escape contexts
47+
private enum EscapeContext {
48+
49+
/// A enumeration of potential html contexts
50+
internal enum Context {
51+
52+
/// Consider an attribute value
53+
case attribute
54+
55+
/// Consider an element body
56+
case element
57+
}
58+
59+
/// Consider an html context
60+
case html(Context)
61+
62+
/// Consider a css context
63+
case css
64+
65+
/// Consider a js context
66+
case js
67+
}
68+
4669
/// The context environment used during rendering
4770
private var environment: Environment
4871

@@ -58,8 +81,11 @@ public struct Renderer {
5881
/// The logger used to log all operations
5982
private var logger: Logger
6083

61-
/// The encoder used to encode html entities
84+
/// The encoder used to encode html entities
6285
private var encoder: Encoder
86+
87+
/// The sanitizer used to clean up html output
88+
private var sanitizer: Sanitizer
6389

6490
/// Initializes the renderer
6591
///
@@ -79,6 +105,7 @@ public struct Renderer {
79105
self.features = features
80106
self.logger = logger
81107
self.encoder = Encoder()
108+
self.sanitizer = Sanitizer()
82109
}
83110

84111
/// Renders the provided view
@@ -132,7 +159,7 @@ public struct Renderer {
132159
try render(modifier: modifier, on: &result)
133160

134161
case let value as EnvironmentValue:
135-
result += escape(content: try render(envvalue: value))
162+
result += escape(try render(envvalue: value), as: .html(.element))
136163

137164
case let statement as Statement:
138165
try render(statement: statement, on: &result)
@@ -146,7 +173,7 @@ public struct Renderer {
146173
case let string as MarkdownString:
147174

148175
if !features.contains(.markdown) {
149-
result += escape(content: string.raw)
176+
result += escape(string.raw, as: .html(.element))
150177

151178
} else {
152179
result += try render(markstring: string)
@@ -158,6 +185,12 @@ public struct Renderer {
158185
case let string as HtmlString:
159186
result += string.value
160187

188+
case let string as JsString:
189+
result += escape(string.value, as: .js)
190+
191+
case let string as CssString:
192+
result += escape(string.value, as: .css)
193+
161194
case let doubleValue as Double:
162195
result += String(doubleValue)
163196

@@ -168,7 +201,7 @@ public struct Renderer {
168201
result += String(intValue)
169202

170203
case let stringValue as String:
171-
result += escape(content: stringValue)
204+
result += escape(stringValue, as: .html(.element))
172205

173206
case let date as Date:
174207

@@ -246,7 +279,7 @@ public struct Renderer {
246279

247280
result += "<!--"
248281

249-
result += escape(content: element.content)
282+
result += escape(element.content, as: .html(.element))
250283

251284
result += "-->"
252285
}
@@ -426,10 +459,10 @@ public struct Renderer {
426459
result += try render(localized: string)
427460

428461
case let value as EnvironmentValue:
429-
result += escape(attribute: try render(envvalue: value))
462+
result += escape(try render(envvalue: value), as: .html(.attribute))
430463

431464
default:
432-
result += escape(attribute: "\(attribute.value)")
465+
result += escape("\(attribute.value)", as: .html(.attribute))
433466
}
434467

435468
result += "\""
@@ -442,7 +475,7 @@ public struct Renderer {
442475
///
443476
/// - Returns: The string representation
444477
private func render(markstring: MarkdownString) throws -> String {
445-
return markdown.render(string: escape(content: markstring.raw))
478+
return markdown.render(string: escape(markstring.raw, as: .html(.element)))
446479
}
447480

448481
/// Renders a environment string
@@ -508,7 +541,7 @@ public struct Renderer {
508541
result += string.value
509542

510543
case let string as String:
511-
result += escape(content: string)
544+
result += escape(string, as: .html(.element))
512545

513546
case is EnvironmentValue:
514547

@@ -600,35 +633,43 @@ public struct Renderer {
600633
try render(loop: envstring.values, with: value, on: &result)
601634
}
602635

603-
/// Escapes special characters in the given attribute value
604-
///
605-
/// The special characters for the attribute are the backslash, the ampersand and the single quotation mark.
606-
///
607-
/// - Parameter value: The attribute value to be escaped
608-
///
609-
/// - Returns: The escaped value
610-
private func escape(attribute value: String) -> String {
636+
/// Escapes a string according to the given context.
637+
///
638+
/// - Parameter value: The string value to escape
639+
/// - Parameter context: The context that defines the escaping
640+
///
641+
/// - Returns: The escaped string
642+
private func escape(_ value: String, as context: EscapeContext) -> String {
611643

612644
if !features.contains(.escaping) {
613645
return value
614646
}
615647

616-
return encoder.encode(value, as: .html(.attribute))
617-
}
618-
619-
/// Escapes special characters in the given content value
620-
///
621-
/// The special characters for the content are the Greater than, Less than symbol.
622-
///
623-
/// - Parameter value: The content value to be escaped
624-
///
625-
/// - Returns: The escaped value
626-
private func escape(content value: String) -> String {
648+
switch context {
649+
case .html(let context):
650+
651+
switch context {
652+
case .attribute:
653+
return encoder.encode(value, as: .html(.attribute))
654+
655+
case .element:
656+
return encoder.encode(value, as: .html(.body))
657+
}
658+
659+
case .css:
660+
661+
// To prevent breaking out of context
662+
let sanitized = sanitizer.strip("style", from: value)
627663

628-
if !features.contains(.escaping) {
629-
return value
664+
return encoder.encode(sanitized, as: .css)
665+
666+
case .js:
667+
668+
// To prevent breaking out of context
669+
let sanitized = sanitizer.strip("script", from: value)
670+
671+
return encoder.encode(sanitized, as: .js)
672+
630673
}
631-
632-
return encoder.encode(value, as: .html(.body))
633674
}
634675
}

Tests/HTMLKitTests/SecurityTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,41 @@ final class SecurityTests: XCTestCase {
149149
"""
150150
)
151151
}
152+
153+
/// Tests the encoding within a javascript context
154+
///
155+
/// The renderer is expected to remove unallowed tags and to encode only string literal
156+
func testEncodingJsContext() throws {
157+
158+
let view = TestView {
159+
Script {
160+
"</script><script> var test = '<b>attack</b>';"
161+
}
162+
}
163+
164+
XCTAssertEqual(try renderer.render(view: view),
165+
"""
166+
<script> var test = '\\u003Cb\\u003Eattack\\u003C/b\\u003E';</script>
167+
"""
168+
)
169+
}
170+
171+
/// Tests the encoding within a css context
172+
///
173+
/// The renderer is expected to remove unallowed tags and to encode only string literal
174+
func testEncodingCssContext() throws {
175+
176+
let view = TestView {
177+
Style {
178+
"</style><style> p { content: '<b>attack</b>'; }"
179+
}
180+
}
181+
182+
XCTAssertEqual(try renderer.render(view: view),
183+
"""
184+
<style> p { content: '\\00003Cb\\00003Eattack\\00003C/b\\00003E'; }</style>
185+
"""
186+
)
187+
}
152188
}
153189

0 commit comments

Comments
 (0)