Skip to content

Commit ec5f13f

Browse files
authored
fix: (critical) prevent xss in unicode input component (#735)
This PR fixes a prompt injection in the unicode input component, which is also used on several web pages (e.g. Loogle). The issue was originally reported [here](https://leanprover.zulipchat.com/#narrow/channel/113488-general/topic/weird.20behavior.20in.20loogle.20searchbar/with/578558458). The technical cause of the issue is as follows: - The text of the unicode input HTML element is set (e.g. from a query parameter) and escaped properly - Any of the handlers in the unicode input component triggers, reading the `innerText` of the component, which yields the text of the HTML element (without escapes) - The unicode input component sets the `innerHtml` of the HTML element (potentially including tags to mark active abbreviations using underlines), thus injecting the non-escaped tags The fix for this issue is to escape all text before setting it in the `innerHtml` except for the underline tags that are created by the unicode input component itself. **If you are using the unicode input component on a website, it is strongly recommended to update the unicode input component to 0.2.0 after this PR!**
1 parent a884fa0 commit ec5f13f

File tree

1 file changed

+13
-4
lines changed
  • lean4-unicode-input-component/src

1 file changed

+13
-4
lines changed

lean4-unicode-input-component/src/index.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,25 @@ function setTextCursorSelection(searchNode: Node, offset: number) {
122122
sel.addRange(range)
123123
}
124124

125+
function escapeHtml(s: string): string {
126+
return s
127+
.replaceAll('&', '&')
128+
.replaceAll('<', '&lt;')
129+
.replaceAll('>', '&gt;')
130+
.replaceAll('"', '&quot;')
131+
.replaceAll("'", '&#039;')
132+
}
133+
125134
function replaceAt(str: string, updates: { range: Range; update: (old: string) => string }[]): string {
126135
updates.sort((u1, u2) => u1.range.offset - u2.range.offset)
127136
let newStr = ''
128137
let lastUntouchedPos = 0
129138
for (const u of updates) {
130-
newStr += str.slice(lastUntouchedPos, u.range.offset)
139+
newStr += escapeHtml(str.slice(lastUntouchedPos, u.range.offset))
131140
newStr += u.update(str.slice(u.range.offset, u.range.offsetEnd + 1))
132141
lastUntouchedPos = u.range.offset + u.range.length
133142
}
134-
newStr += str.slice(lastUntouchedPos)
143+
newStr += escapeHtml(str.slice(lastUntouchedPos))
135144
return newStr
136145
}
137146

@@ -220,7 +229,7 @@ export class InputAbbreviationRewriter implements AbbreviationTextSource {
220229
const queryHtml = this.textInput.innerHTML
221230
const updates = Array.from(this.rewriter.getTrackedAbbreviations()).map(a => ({
222231
range: a.range,
223-
update: (old: string) => `<u>${old}</u>`,
232+
update: (old: string) => `<u>${escapeHtml(old)}</u>`,
224233
}))
225234
const newQueryHtml = replaceAt(query, updates)
226235
if (queryHtml === newQueryHtml) {
@@ -236,7 +245,7 @@ export class InputAbbreviationRewriter implements AbbreviationTextSource {
236245
async replaceAbbreviations(changes: Change[]): Promise<boolean> {
237246
const updates: { range: Range; update: (old: string) => string }[] = changes.map(c => ({
238247
range: c.range,
239-
update: _ => c.newText,
248+
update: _ => escapeHtml(c.newText),
240249
}))
241250
this.setInputHTML(replaceAt(this.getInput(), updates))
242251
// Unlike in VS Code, directly setting innerHTML does not fire any document-change events,

0 commit comments

Comments
 (0)