Skip to content

Commit 36b7bca

Browse files
committed
Apply the correct encoding
1 parent 5aea80c commit 36b7bca

7 files changed

Lines changed: 128 additions & 88 deletions

File tree

Sources/HTMLKit/Abstraction/Attributes/BasicAttributes.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3482,14 +3482,14 @@ public protocol StyleAttribute: Attribute {
34823482
extension StyleAttribute where Self: ContentNode {
34833483

34843484
internal func mutate(style value: String) -> Self {
3485-
return self.mutate(key: "style", value: value)
3485+
return self.mutate(key: "style", value: TaintedString(value, as: .css(.attribute)))
34863486
}
34873487
}
34883488

34893489
extension StyleAttribute where Self: EmptyNode {
34903490

34913491
internal func mutate(style value: String) -> Self {
3492-
return self.mutate(key: "style", value: value)
3492+
return self.mutate(key: "style", value: TaintedString(value, as: .css(.attribute)))
34933493
}
34943494
}
34953495

Sources/HTMLKit/Abstraction/Elements/BodyElements.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4559,7 +4559,7 @@ extension Paragraph: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttribut
45594559
}
45604560

45614561
public func on(event: Events.Mouse, _ value: String) -> Paragraph {
4562-
return mutate(key: event.rawValue, value: value)
4562+
return mutate(key: event.rawValue, value: TaintedString(value, as: .js(.attribute)))
45634563
}
45644564

45654565
public func on(event: Events.Wheel, _ value: String) -> Paragraph {
@@ -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 = [TaintedString(content().joined(), as: .js)]
21141+
self.content = [TaintedString(content().joined(), as: .js(.element))]
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 = [TaintedString(content().joined(), as: .css)]
718+
self.content = [TaintedString(content().joined(), as: .css(.element))]
719719
}
720720

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

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

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import Foundation
22

3-
/// A type that represents an encoder
3+
/// A type that represents an encoder.
44
///
55
/// The encoder is responsible for encoding characters to their safe equivalents.
6+
///
7+
/// > Note: The encoder does not utilize unicode encoding for some languages, such as JS and CSS.
8+
/// > It primarily aims to preserve the code integrity and to protect against code injection within an
9+
/// > HTML context.
610
internal struct Encoder {
711

8-
/// A enumeration of potential encoding mechanism
12+
/// An enumeration of potential encoding mechanism
913
internal enum Mechanism {
10-
14+
1115
/// The context the mechanism should be based on
1216
internal enum Context {
1317

@@ -18,14 +22,14 @@ internal struct Encoder {
1822
case element
1923
}
2024

21-
/// Use a html encoding mechanism
25+
/// Use a HTML encoding mechanism
2226
case html(Context)
2327

24-
/// Use a css encoding mechanism
25-
case css
28+
/// Use a CSS encoding mechanism
29+
case css(Context)
2630

27-
/// Use a js encoding mechanism
28-
case js
31+
/// Use a JS encoding mechanism
32+
case js(Context)
2933

3034
/// The characters to replace based on the mechanism and context
3135
var characters: Set<Unicode.Scalar> {
@@ -35,48 +39,37 @@ internal struct Encoder {
3539

3640
switch context {
3741
case .attribute:
38-
return ["&", "'", "\""]
42+
return ["&", "<", "'", "\""]
43+
44+
case .element:
45+
return ["&", "<", ">"]
46+
}
47+
48+
case .css(let context):
49+
50+
switch context {
51+
case .attribute:
52+
return ["&", "<", ">", "\""]
3953

4054
case .element:
4155
return ["<", ">"]
4256
}
4357

44-
case .css:
45-
return ["<", ">"]
58+
case .js(let context):
4659

47-
case .js:
48-
return ["<", ">"]
60+
switch context {
61+
case .attribute:
62+
return ["&", "<", ">", "\""]
63+
64+
case .element:
65+
return ["<", ">"]
66+
}
4967
}
5068
}
5169

5270
/// The characters to replace with based on the mechanism
5371
var replacements: [Unicode.Scalar: String] {
54-
55-
switch self {
56-
case .html:
57-
return ["&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&apos;"]
58-
59-
case .css:
60-
return ["<": "\\00003C", ">": "\\00003E"]
61-
62-
case .js:
63-
return ["<": "\\u003C", ">": "\\u003E"]
64-
}
65-
}
66-
67-
/// The delimiters to look for while encoding
68-
var delimiters: Set<Unicode.Scalar> {
69-
70-
switch self {
71-
case .html:
72-
return []
73-
74-
case .css:
75-
return ["\"", "'"]
76-
77-
case .js:
78-
return ["\"", "'", "`"]
79-
}
72+
return ["&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&apos;"]
8073
}
8174
}
8275

@@ -90,15 +83,22 @@ internal struct Encoder {
9083
internal func encode(_ string: String, as mechansim: Mechanism) -> String {
9184

9285
switch mechansim {
93-
case .css, .js:
94-
return self.replace(mechansim.characters, with: mechansim.replacements, within: mechansim.delimiters, on: string)
86+
case .css(let context), .js(let context):
87+
88+
switch context {
89+
case .attribute:
90+
return self.replace(mechansim.characters, with: mechansim.replacements, on: string)
91+
92+
case .element:
93+
return self.replace(mechansim.characters, with: mechansim.replacements, within: ["\"", "'"], on: string)
94+
}
9595

9696
case .html:
9797
return self.replace(mechansim.characters, with: mechansim.replacements, on: string)
9898
}
9999
}
100100

101-
/// Replaces occurrences of characters in a string with their corresponding replacements
101+
/// Replaces occurrences of characters within a substring with their corresponding replacements.
102102
///
103103
/// - Parameters:
104104
/// - set: The set of character to check against
@@ -137,14 +137,14 @@ internal struct Encoder {
137137
if set.contains(scalar) {
138138

139139
if let replacement = table[scalar] {
140-
result.append(contentsOf: replacement)
140+
result.append(replacement)
141141

142142
} else {
143-
result.append(contentsOf: String(scalar))
143+
result.append(String(scalar))
144144
}
145145

146146
} else {
147-
result.append(contentsOf: String(scalar))
147+
result.append(String(scalar))
148148
}
149149

150150
} else {
@@ -156,7 +156,7 @@ internal struct Encoder {
156156
return result
157157
}
158158

159-
/// Replaces occurrences of characters in a string with their corresponding replacements
159+
/// Replaces occurrences of characters within a string with their corresponding replacements.
160160
///
161161
/// - Parameters:
162162
/// - set: The set of characters to check against
@@ -173,14 +173,14 @@ internal struct Encoder {
173173
if set.contains(scalar) {
174174

175175
if let replacement = table[scalar] {
176-
result.append(contentsOf: replacement)
176+
result.append(replacement)
177177

178178
} else {
179-
result.append(contentsOf: String(scalar))
179+
result.append(String(scalar))
180180
}
181181

182182
} else {
183-
result.append(contentsOf: String(scalar))
183+
result.append(String(scalar))
184184
}
185185
}
186186

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
/// A string that represents a untrusted string
1+
/// A type that represents an untrusted string.
22
///
3-
/// The tainted string comes from a untrusted source and must be escaped by the renderer,
3+
/// The tainted string comes from an untrusted source and must be escaped by the renderer,
44
/// before it is getting consumed.
55
internal struct TaintedString: Content {
66

7+
/// An enumeration of potential source contexts
78
internal enum Context {
89

9-
/// The context within a html context
10-
internal enum Context {
10+
/// The subcontext within a source context
11+
internal enum Subcontext {
1112

1213
/// Consider an attribute value
1314
case attribute
@@ -16,14 +17,14 @@ internal struct TaintedString: Content {
1617
case element
1718
}
1819

19-
/// Consider an html context
20-
case html(Context)
20+
/// Consider an HTML context
21+
case html(Subcontext)
2122

22-
/// Consider a css context
23-
case css
23+
/// Consider a CSS context
24+
case css(Subcontext)
2425

25-
/// Consider a js context
26-
case js
26+
/// Consider a JS context
27+
case js(Subcontext)
2728
}
2829

2930
/// The string value marked as tainted
@@ -32,7 +33,7 @@ internal struct TaintedString: Content {
3233
/// The context of the source of the string
3334
internal let context: Context
3435

35-
/// Create a tainted string
36+
/// Create a tainted string.
3637
///
3738
/// - Parameters:
3839
/// - value: The string value

Sources/HTMLKit/Framework/Rendering/Renderer.swift

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,9 @@ public struct Renderer {
435435
case let value as EnvironmentValue:
436436
result += escape(tainted: .init(try render(envvalue: value), as: .html(.attribute)))
437437

438+
case let string as TaintedString:
439+
result += escape(tainted: string)
440+
438441
default:
439442
result += escape(tainted: .init("\(attribute.value)", as: .html(.attribute)))
440443
}
@@ -612,7 +615,7 @@ public struct Renderer {
612615

613616
/// Escapes a tainted string
614617
///
615-
/// It takes precautions such as character encoding and input sanitization to ensure a safe output. Though the escaping alone
618+
/// It takes precautions such as output encoding and input sanitization to ensure a safe output. Though the escaping alone
616619
/// does not guarantee complete safety. It only helps mitigate the risk.
617620
///
618621
/// - Parameter string: The string value to escape.
@@ -627,30 +630,43 @@ public struct Renderer {
627630
}
628631

629632
switch string.context {
630-
case .html(let context):
633+
case .html(let subcontext):
631634

632-
switch context {
635+
switch subcontext {
633636
case .attribute:
634637
return encoder.encode(string.value, as: .html(.attribute))
635638

636639
case .element:
637640
return encoder.encode(string.value, as: .html(.element))
638641
}
639642

640-
case .css:
643+
case .css(let subcontext):
641644

642-
// To prevent breaking out of context
643-
let sanitized = sanitizer.strip("style", from: string.value)
644-
645-
return encoder.encode(sanitized, as: .css)
645+
switch subcontext {
646+
case .attribute:
647+
return encoder.encode(string.value, as: .css(.attribute))
648+
649+
case .element:
650+
651+
// To prevent breaking out of context
652+
let sanitized = sanitizer.strip("style", from: string.value)
646653

647-
case .js:
654+
return encoder.encode(sanitized, as: .css(.element))
655+
}
648656

649-
// To prevent breaking out of context
650-
let sanitized = sanitizer.strip("script", from: string.value)
651-
652-
return encoder.encode(sanitized, as: .js)
657+
case .js(let subcontext):
653658

659+
switch subcontext {
660+
case .attribute:
661+
return encoder.encode(string.value, as: .js(.attribute))
662+
663+
case .element:
664+
665+
// To prevent breaking out of context
666+
let sanitized = sanitizer.strip("script", from: string.value)
667+
668+
return encoder.encode(sanitized, as: .js(.element))
669+
}
654670
}
655671
}
656672
}

0 commit comments

Comments
 (0)