Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 63 additions & 26 deletions src/playlist_new.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ function getId(id) {
return document.getElementById(id);
}

function sanitizeHttpUrl(value) {
try {
const parsedUrl = new URL(value);
if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") {
return parsedUrl.href;
}
} catch (error) {}
return "";
}

function pasteLink() {
const clipboardText = clipboard.readText();
getId("loadingWrapper").style.display = "flex";
Expand All @@ -67,19 +77,17 @@ function pasteLink() {
getId("incorrectMsg").textContent = i18n.__(
"Some error has occurred. Check your network and use correct URL"
);
getId("errorDetails").innerHTML = `
<strong>URL: ${clipboardText}</strong>
<br><br>
${error}
`;
getId("errorDetails").textContent = `URL: ${clipboardText}\n\n${error}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the innerHTML-with-URL sink still exists in the sibling files and find any other instances.
rg -nP --type=js -C2 'innerHTML\s*=\s*`[^`]*\$\{[^}]*\b(url|URL)\b'
rg -nP --type=js -C2 '\.innerHTML\s*=' -g '!**/node_modules/**'

Repository: aandrew-me/ytDownloader

Length of output: 3730


Sibling files still contain the same DOM XSS vulnerability.

This PR fixes the vulnerability in src/playlist_new.js by switching from innerHTML to textContent, but the identical vulnerable pattern remains in:

  • src/renderer.js:1557: errorDetails.innerHTML = \URL: ${url}...``
  • src/playlist.js:584–586: this.ui.errorDetails.innerHTML = \URL: ${this.state.url}...``

Both files directly interpolate untrusted URL/url variables into HTML. If the PR treats this as critical (DOM XSS → RCE via Node integration), the fix should extend to these sibling locations or be tracked as a follow-up issue linked to this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/playlist_new.js` at line 80, Multiple sibling locations still assign
untrusted URL values into innerHTML (e.g., errorDetails.innerHTML,
this.ui.errorDetails.innerHTML) causing DOM XSS; change those assignments to use
textContent (or properly escape/sanitize the interpolated values) instead of
innerHTML so the URL/url variables are not interpreted as HTML. Locate the
occurrences that use errorDetails.innerHTML and this.ui.errorDetails.innerHTML
and replace the HTML interpolation with plain text assignments (or call a
safe-escape helper) to ensure the untrusted this.state.url / url values are
rendered as text.

getId("errorDetails").title = i18n.__("Click to copy");
getId("errorBtn").style.display = "inline-block";
} else {
const parsed = JSON.parse(stdout);
console.log(parsed);
let items = "";
const data = getId("data");
data.textContent = "";
// If correct playlist url is used
if (parsed.entries) {
const fragment = document.createDocumentFragment();
parsed.entries.forEach((entry) => {
console.log(entry)
const randId = Math.random()
Expand All @@ -88,23 +96,56 @@ function pasteLink() {
.slice(2);

if (entry.channel) {
items += `
<div class="item" id="${randId}">
<img src="${
entry.thumbnails[3].url
}" alt="No thumbnail" class="itemIcon" crossorigin="anonymous">

<div class="itemBody">
<div class="itemTitle">${entry.title}</div>
<div>${formatTime(entry.duration)}</div>
<input type="checkbox" class="playlistCheck" id="c${randId}">
<input type="hidden" id="link${randId}" value="${entry.url}">
</div>
</div>
`;
const item = document.createElement("div");
item.className = "item";
item.id = randId;

const image = document.createElement("img");
let thumbnailUrl = "";
if (
entry.thumbnails &&
entry.thumbnails[3] &&
entry.thumbnails[3].url
) {
thumbnailUrl = sanitizeHttpUrl(entry.thumbnails[3].url);
}
if (thumbnailUrl) {
image.src = thumbnailUrl;
}
image.alt = "No thumbnail";
image.className = "itemIcon";
image.setAttribute("crossorigin", "anonymous");

const itemBody = document.createElement("div");
itemBody.className = "itemBody";

const itemTitle = document.createElement("div");
itemTitle.className = "itemTitle";
itemTitle.textContent = entry.title || "";

const duration = document.createElement("div");
duration.textContent = formatTime(entry.duration);

const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.className = "playlistCheck";
checkbox.id = `c${randId}`;

const hiddenLink = document.createElement("input");
hiddenLink.type = "hidden";
hiddenLink.id = `link${randId}`;
hiddenLink.value = entry.url || "";

itemBody.appendChild(itemTitle);
itemBody.appendChild(duration);
itemBody.appendChild(checkbox);
itemBody.appendChild(hiddenLink);
item.appendChild(image);
item.appendChild(itemBody);
fragment.appendChild(item);
}
});
getId("data").innerHTML = items;
data.appendChild(fragment);
getId("loadingWrapper").style.display = "none";
}
// If correct playlist url is not used
Expand All @@ -113,11 +154,7 @@ function pasteLink() {
getId("incorrectMsg").textContent = i18n.__(
"Incompatible URL. Please provide a playlist URL"
);
getId("errorDetails").innerHTML = `
<strong>URL: ${clipboardText}</strong>
<br><br>
${error}
`;
getId("errorDetails").textContent = `URL: ${clipboardText}\n\n${stderr}`;
getId("errorDetails").title = i18n.__("Click to copy");
getId("errorBtn").style.display = "inline-block";
}
Expand Down