Summary
A navbar-loading script in an Electron desktop application fetched a local navbar.html fragment and inserted it directly into the page using innerHTML, with no sanitization step in between. Because Electron renderers can run with Node.js integration enabled, any HTML or JavaScript smuggled into that fragment — through a compromised dependency, a corrupted installer, or local file tampering — could execute with the same privileges as the app itself. The fix rewrites the render path to strip <script> tags and on*/srcdoc attributes from the fetched markup before it ever touches the DOM.
Affected Versions
| Affected | not applicable (first-party code) |
| Fixed in | not applicable (first-party code) — fixed by the PR described below |
| Ecosystem | n/a (Electron/JavaScript, first-party script) |
| CVE / GHSA | not assigned |
| CWE | unknown |
Since this is application code rather than a published package, there's no version range to check — if your build includes the pre-fix loadNavbar.js, you're exposed; once the PR is merged, you aren't.
The Vulnerability Explained
The vulnerable logic lived in the navbar bootstrap routine that runs on DOMContentLoaded. It fetched a local HTML fragment and wrote the response straight into the DOM:
fetch("navbar.html")
.then((response) => response.text())
.then((data) => {
document.getElementById("navbar").innerHTML = data;
The problem is the unconditional innerHTML = data assignment. Whatever bytes come back from navbar.html are parsed and executed as live DOM content — including <script> tags and inline event-handler attributes like onerror or onload. There is no check that the fetched content is the trusted, unmodified navbar markup shipped with the app.
In a desktop app context, the attack doesn't need a network man-in-the-middle. The threat model described in the fix is local: if navbar.html is replaced on disk — by a compromised npm dependency during install, a tampered auto-update package, or any process with local file write access — the next time the app starts, that modified file is fetched and rendered without question. A payload as simple as:
<img src=x onerror="require('child_process').exec('calc.exe')">
would execute the moment the broken image fails to load, because the handler runs inside the Electron renderer. If that renderer has Node integration enabled (common in older or loosely configured Electron apps), require is directly reachable from DOM event handlers, turning a markup-injection bug into full code execution on the user's machine — spawning processes, reading files, or reaching out to the network, all under the identity of the desktop app.
The Fix
The patch adds a sanitizeNavbarHtml helper that runs the fetched text through DOMParser, then strips the dangerous parts before anything is attached to the live document:
const sanitizeNavbarHtml = (html) => {
const doc = new DOMParser().parseFromString(html, "text/html");
doc.querySelectorAll("script").forEach((el) => el.remove());
doc.querySelectorAll("*").forEach((el) => {
[...el.attributes].forEach((attr) => {
if (/^on/i.test(attr.name) || attr.name === "srcdoc") {
el.removeAttribute(attr.name);
}
});
});
return doc.body.innerHTML;
};
The render call then uses the sanitized output instead of the raw response:
document.getElementById("navbar").innerHTML = sanitizeNavbarHtml(data);
This matters because the parsing happens in a detached document created by DOMParser — nothing executes while the markup is being inspected and cleaned. Only after <script> elements are removed and every on*/srcdoc attribute is stripped from every element does the resulting HTML get written into the real, live #navbar container. That ordering is what neutralizes the <img onerror="..."> style payload described in the exploit scenario: the onerror attribute is gone before the <img> tag ever reaches the DOM that the browser actually evaluates.
Key Takeaways
innerHTML = fetchedTextis equivalent to runningeval()on whatever that fetch returns — treat any locally fetched.htmlfragment as untrusted input, not just data pulled from the network.- Sanitizing with
DOMParserplus explicit<script>andon*/srcdocstripping is a targeted fix for this payload shape, but it's not a substitute for a full sanitizer if the navbar markup ever grows more complex attributes (e.g.,javascript:URLs inhref/src). - In Electron apps, a markup-injection bug is far more dangerous than in a browser tab because
require('child_process')and similar Node APIs may be reachable from renderer-side event handlers — disabling Node integration in renderers is a complementary mitigation worth checking alongside this fix. - Sanitizing the render path doesn't verify the integrity of
navbar.htmlitself; an attacker who can still overwrite that file on disk retains some influence over what's displayed, even if script execution is blocked.
How Orbis AppSec Detected This
- Source: the
fetch("navbar.html")response body, read viaresponse.text() - Sink:
document.getElementById("navbar").innerHTML = data - Missing control: no HTML sanitization or script/attribute stripping between fetching the file and injecting it into the live DOM
- CWE: unknown (not assigned for this finding)
- Fix: a new
sanitizeNavbarHtmlfunction parses the fetched markup withDOMParser, removes<script>elements andon*/srcdocattributes, and only then assigns the result toinnerHTML
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This finding is a reminder that "local" content isn't automatically trusted content. navbar.html was fetched from the app's own filesystem, which made it easy to assume it was safe to pour straight into innerHTML — but anything that can modify a file on disk, from a compromised dependency to a corrupted update, can turn that assumption into renderer-level code execution. The fix doesn't change what the navbar looks like to a legitimate user; it just makes sure that if navbar.html is ever tampered with, the <script> tags and event handlers an attacker would rely on never survive the trip from fetch() to the DOM.