Commit 0da1aee
committed
[security] Always sanitize RichText output for safe-HTML output type
Stored XSS: RichTextValue.output returned the raw, unsanitized stored
value whenever mimeType == outputMimeType, skipping the safe_html
transform. Because the safe-HTML output type (text/x-html-safe) is the
type that signifies "already sanitized", any value whose stored mimeType
equals it bypassed sanitization entirely on render. The safe_html
transform itself is sound (it strips on* event-handler attributes and
javascript:/data: URIs); the defect is that the transform was never
invoked for these values.
Two reproduction entrypoints reach this skip with attacker-controlled
input:
A. Lazy constructor. RichTextValue("<img src=x onerror=alert(1)>")
defaults both mimeType and outputMimeType to None. None == None
satisfies the shortcut, so .output returns the raw payload. A
developer doing the obvious thing to set a RichText field
programmatically silently disables sanitization.
B. REST deserialization. A client POSTs a RichText field as
{"data": "<img src=x onerror=alert(1)>",
"content-type": "text/x-html-safe"}. The deserializer trusts the
client-supplied content-type and constructs a RichTextValue whose
mimeType equals outputMimeType, again hitting the skip. No
developer code is involved; the framework builds the unsanitized
value from the request.
The stored value is later rendered with tal:content="structure ...",
which performs no escaping or sanitization, so the payload executes in
the victim's browser. Any user who can set a RichText field (or reach
the REST endpoint) can store a payload that fires for every viewer of
the rendered content.
Fix: in RichTextValue.output_relative_to, when the (effective) output
type is the safe-HTML type, do not honor the mimeType == outputMimeType
shortcut. Instead treat the input as text/html and run the safe_html
transform. The transform is idempotent for genuinely-safe markup, so
legitimate already-sanitized content is unaffected, while
attacker-controlled input that merely claims to be text/x-html-safe is
sanitized. The no-op shortcut is preserved for equal non-safe mimetypes
(e.g. text/plain -> text/plain). None raw values short-circuit to None.
Adds regression tests for both entrypoints: the lazy constructor
(defaulted None mimetypes) and a value whose input mimeType spoofs the
safe-HTML output type. Both assert the onerror payload is absent from
the rendered output. The REST-deserializer entrypoint is covered by a
companion test in plone.restapi, which avoids a test-time dependency on
plone.1 parent 9a83791 commit 0da1aee
2 files changed
Lines changed: 64 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
50 | 83 | | |
51 | 84 | | |
52 | 85 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
12 | 15 | | |
13 | 16 | | |
14 | 17 | | |
| |||
84 | 87 | | |
85 | 88 | | |
86 | 89 | | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
87 | 98 | | |
88 | 99 | | |
89 | 100 | | |
| |||
93 | 104 | | |
94 | 105 | | |
95 | 106 | | |
96 | | - | |
| 107 | + | |
97 | 108 | | |
98 | 109 | | |
99 | 110 | | |
| |||
103 | 114 | | |
104 | 115 | | |
105 | 116 | | |
106 | | - | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
107 | 124 | | |
108 | 125 | | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
109 | 133 | | |
110 | 134 | | |
111 | 135 | | |
112 | 136 | | |
113 | 137 | | |
114 | 138 | | |
115 | 139 | | |
116 | | - | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
117 | 145 | | |
118 | 146 | | |
119 | 147 | | |
| |||
0 commit comments