Skip to content

fix: use innerHTML instead of textContent in createElement helper#92

Open
simgunz wants to merge 3 commits into
awslabs:devfrom
simgunz:fix/createElement-innerHTML-rendering
Open

fix: use innerHTML instead of textContent in createElement helper#92
simgunz wants to merge 3 commits into
awslabs:devfrom
simgunz:fix/createElement-innerHTML-rendering

Conversation

@simgunz
Copy link
Copy Markdown
Contributor

@simgunz simgunz commented Mar 17, 2026

Issue #, if available: N/A

Description of changes:

The createElement utility in main.js names its third parameter innerHTML but assigns it via el.textContent, which escapes HTML. This causes dynamically injected elements (report filter controls, non-match table rows, PDF navigation buttons) to render as raw HTML strings instead of interactive UI components.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Before the fix:
Screenshot_20260317_134132

const el = document.createElement(tag);
if (className) el.className = className;
if (innerHTML) el.textContent = innerHTML;
if (innerHTML) el.innerHTML = innerHTML;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate this! One thing to note, now that this uses innerHTML, a couple of the call sites pass interpolated data without escaping:

  • updateNonMatchesTable (line 379) - row.doc_id, row.field_path, etc. go straight into <td> tags
  • loadPDF (line 541) - docId gets interpolated into onclick handlers, which could allow string breakout

There's already an escapeHtml helper at line 394, would you mind wrapping those interpolations with it? Low risk since it's a local report, but good to close the door on it while we're here. Really appreciate it!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Comment on lines 394 to 398
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks for adding the escaping! One more thing, the escapeHtml is defined locally inside updateDocumentFiles here, but it's now called from updateNonMatchesTable (line 380) and loadPDF (line 541) too, which are separate functions. Those calls will hit a ReferenceError at runtime since it's not in scope there. Would you mind hoisting this up to module scope near the other helpers (around line 29 by createElement)? That way every call site can reach it. Rest of the changes look great!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. It should be ok now.

@adiadd
Copy link
Copy Markdown
Contributor

adiadd commented Apr 13, 2026

Hi @simgunz, appreciate the PR! Any questions or update on the requested changes?

simgunz added 3 commits April 17, 2026 09:19
The createElement helper named its third parameter 'innerHTML' but
assigned it via textContent, causing HTML strings (filter controls,
table rows, PDF navigation) to render as escaped text instead of
being interpreted as HTML.
Move escapeHtml from inside updateDocumentFiles to module scope
next to createElement, making it accessible to all functions.
Wrap user-controlled values with escapeHtml in updateNonMatchesTable
and loadPDF to prevent cross-site scripting via doc_id, field_path,
and other interpolated fields.
@simgunz simgunz force-pushed the fix/createElement-innerHTML-rendering branch from b2dc37f to 5f88ec3 Compare April 21, 2026 09:05
Copy link
Copy Markdown
Contributor

@adiadd adiadd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking another look on this, the textContentinnerHTML flip and the table-row escaping look great. The two spots I'd like to nail down before merging are both in the PDF nav area (left inline comments). Once those are sorted this is good to go. Really appreciate you sticking with the back-and-forth on this one!

Comment on lines +540 to +544
const safeDocId = escapeHtml(docId);
const navControls = createElement('div', 'pdf-navigation', `
<button class="btn btn-primary pdf-nav-btn" id="prev-${docId}" onclick="navigatePDF('${docId}', -1)">← Previous</button>
<span class="pdf-page-info" id="page-info-${docId}">Page 1 of ${pdf.numPages}</span>
<button class="btn btn-primary pdf-nav-btn" id="next-${docId}" onclick="navigatePDF('${docId}', 1)">Next →</button>
<button class="btn btn-primary pdf-nav-btn" id="prev-${safeDocId}" onclick="navigatePDF('${safeDocId}', -1)">← Previous</button>
<span class="pdf-page-info" id="page-info-${safeDocId}">Page 1 of ${pdf.numPages}</span>
<button class="btn btn-primary pdf-nav-btn" id="next-${safeDocId}" onclick="navigatePDF('${safeDocId}', 1)">Next →</button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appreciate the escaping pass, one thing - the escapeHtml helper only covers &, <, > (since it round-trips through textContentinnerHTML). It doesn't escape " or ', which means in this block:

<button ... id="prev-${safeDocId}" onclick="navigatePDF('${safeDocId}', -1)">

a docId with a " in it can break out of the id attribute, and a docId with a ' can break out of the onclick JS string. So the door's still cracked open here.

i think the cleanest fix imo is to ditch the HTML-string template for the nav and build it as DOM + addEventListener, same pattern we're already using in updateDocumentFiles:

const navControls = createElement('div', 'pdf-navigation');
const prev = document.createElement('button');
prev.className = 'btn btn-primary pdf-nav-btn';
prev.id = `prev-${docId}`;
prev.textContent = '← Previous';
prev.addEventListener('click', () => navigatePDF(docId, -1));
// …same for the page-info span and next button, then append all three…

this way there's no HTML parse on user-controlled data at all and we don't need escapeHtml here. ff you'd rather keep the template and do the minimal patch, we'd need to extend escapeHtml to also cover " and ', lmk which you prefer. thanks!


const pdfContainer = createElement('div', 'pdf-container');
const canvas = document.createElement('canvas');
canvas.id = `pdf-canvas-${escapeHtml(docId)}`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

escapeHtml shouldn't be used on these since .id is a DOM property assignment, not an HTML parse. if a docId ever contains & or <, this stores the id as e.g. pdf-canvas-foo&amp;bar literally, but then line 430 / 575 / 601-602 look it up using the raw docId and getElement returns null → PDF never renders and nav buttons don't bind.

should just be raw docId here, same as before:

canvas.id = `pdf-canvas-${docId}`;
loading.id = `pdf-loading-${docId}`;
error.id = `pdf-error-${docId}`;

escaping only matters where the value gets parsed as HTML/attr/JS

canvas.id = `pdf-canvas-${escapeHtml(docId)}`;
canvas.className = 'pdf-canvas';
const loading = createElement('div', 'pdf-loading');
loading.id = `pdf-loading-${escapeHtml(docId)}`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto as the canvas.id

loading.id = `pdf-loading-${escapeHtml(docId)}`;
loading.textContent = 'Loading PDF...';
const error = createElement('div', 'pdf-error');
error.id = `pdf-error-${escapeHtml(docId)}`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto as the canvas.id and the loading.id

@adiadd
Copy link
Copy Markdown
Contributor

adiadd commented May 5, 2026

Hey @simgunz, once again thank you so much for the PR and apologies for the multiple back-and-forth comments! Any questions or update on the requested changes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants