From 441aa114b58eee501832c9e0c54570d9f08595cf Mon Sep 17 00:00:00 2001 From: vict0rsch Date: Mon, 13 Apr 2026 02:25:14 +0200 Subject: [PATCH 01/13] fix: Array.prototype.last mutating original array The .last() implementation called this.reverse() which mutates the array in place. Every call permanently reversed the array, corrupting data when called more than once. Use index-based access instead. Made-with: Cursor --- src/shared/js/utils/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/js/utils/config.js b/src/shared/js/utils/config.js index e516de0a..e4e49a79 100644 --- a/src/shared/js/utils/config.js +++ b/src/shared/js/utils/config.js @@ -6,7 +6,7 @@ import { miniHash } from "@pmu/functions.js"; if (!Array.prototype.last) { Object.defineProperty(Array.prototype, "last", { value: function (i = 0) { - return this.reverse()[i]; + return this[this.length - 1 - i]; }, configurable: true, }); From ae4f3b3606112261850d0ecd53b812d3037c5090 Mon Sep 17 00:00:00 2001 From: vict0rsch Date: Mon, 13 Apr 2026 02:26:16 +0200 Subject: [PATCH 02/13] fix: OpenReview regex, dead code, and MV3 executeScript in background.js - Fix regex capturing only last character of OpenReview IDs by moving the + quantifier inside the capture group. - Remove unreachable dead code in fetchPWCData (after early return). - Replace undefined setTitleCode/setFaviconCode with MV3-compatible chrome.scripting.executeScript using func + args. - Remove unused identifier variable in pushSyncPapers. Made-with: Cursor --- src/background/background.js | 39 ++++++++---------------------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/src/background/background.js b/src/background/background.js index bbc42d52..29701b97 100755 --- a/src/background/background.js +++ b/src/background/background.js @@ -87,7 +87,7 @@ export function initBackground() { // Remove DOM-dependent setFaviconCode since it's not needed in service worker const fetchOpenReviewNoteJSON = async (url) => { - const id = url.match(/id=([\w-])+/)[0].replace("id=", ""); + const id = url.match(/id=([\w-]+)/)[1]; const api = `https://api.openreview.net/notes?id=${id}`; let response = await fetch(api); let json = await response.json(); @@ -101,7 +101,7 @@ export function initBackground() { }; const fetchOpenReviewForumJSON = async (url) => { - const id = url.match(/id=([\w-])+/)[0].replace("id=", ""); + const id = url.match(/id=([\w-]+)/)[1]; const api = `https://api.openreview.net/notes?forum=${id}`; let response = await fetch(api); let json = await response.json(); @@ -161,29 +161,8 @@ export function initBackground() { }; const fetchPWCData = async (arxivId, title) => { - return; // PWC API discontinued, to fix later - let pwcPath = `https://paperswithcode.com/api/v1/papers/?`; - if (arxivId) { - log("Fetching PWC data for arxivId:", arxivId); - pwcPath += new URLSearchParams({ arxiv_id: arxivId }); - } else if (title) { - log("Fetching PWC data for paper:", title); - pwcPath += new URLSearchParams({ title }); - } - const response = await fetch(pwcPath); - try { - const json = await response.json(); - } catch (error) { - logError("[fetchPWCData]", error); - return; - } - - if (json["count"] !== 1) { - log("No PWC entry match."); - return; - } - log("PWC entry match:", json["results"][0]["id"]); - return json["results"][0]; + // PWC API discontinued, to fix later + return; }; const findCodesForPaper = async (request) => { @@ -323,7 +302,6 @@ export function initBackground() { const pushSyncPapers = async () => { if (!(await shouldSync())) return; - const identifier = await getIdentifier(); try { const start = Date.now(); consoleHeader(`Pushing ${String.fromCodePoint("0x23EB")}`); @@ -434,11 +412,10 @@ export function initBackground() { console.log(">>> Setting tab title to :", paperTitle); chrome.scripting.executeScript({ target: { tabId }, - code: ` - ${setTitleCode(paperTitle)}; - ${setFaviconCode}; - `, - runAt: "document_start", + func: (title) => { + document.title = title; + }, + args: [paperTitle], }); } } From 3663695e3e2f53bf15802b96d99599dce48806ee Mon Sep 17 00:00:00 2001 From: vict0rsch Date: Mon, 13 Apr 2026 02:27:16 +0200 Subject: [PATCH 03/13] fix: variable declarations and scoping bugs across files - data.js: add const to major/minor in versionToSemantic (implicit globals) - parsers.js: replace undefined error() with logError() in extractCrossrefData - content_script.js: add const to abstractH2 in huggingfacePapers - options.js: add i parameter to validateImportPaper, add const to pwcMatch Made-with: Cursor --- src/content_scripts/content_script.js | 2 +- src/options/options.js | 6 +++--- src/shared/js/utils/data.js | 4 ++-- src/shared/js/utils/parsers.js | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/content_scripts/content_script.js b/src/content_scripts/content_script.js index 21e2c766..7f17a38c 100644 --- a/src/content_scripts/content_script.js +++ b/src/content_scripts/content_script.js @@ -653,7 +653,7 @@ const huggingfacePapers = (paper, url) => { (PaperMemory) `; - abstractH2 = queryAll("h2").find((h) => h.innerText.trim() === "Abstract"); + const abstractH2 = queryAll("h2").find((h) => h.innerText.trim() === "Abstract"); if (!abstractH2) { log("Missing 'Abstract' h2 title on HuggingFace paper page."); } diff --git a/src/options/options.js b/src/options/options.js index 3611fce4..805bc1c2 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -140,7 +140,7 @@ const handleSelectImportJson = () => { findEl({ element: "import-json-papers-button" }).disabled = false; }; -const validateImportPaper = (p) => { +const validateImportPaper = (p, i) => { if (typeof p === "string") { if (!isValidHttpUrl(p)) { alert(`${p} (entry ${i}) is not a valid URL`); @@ -213,7 +213,7 @@ const handleParseImportJson = async (e) => { throw new Error("The JSON file must contain a *list* of papers"); } for (const [i, p] of papersToParse.entries()) { - if (!validateImportPaper(p)) { + if (!validateImportPaper(p, i)) { return; } } @@ -561,7 +561,7 @@ const startMatching = async (papersToMatch) => { var bibtex, venue, note, code, match; setHTML("matching-status-provider", "paperswithcode.org ..."); - pwcMatch = await tryPWCMatch(paper); + const pwcMatch = await tryPWCMatch(paper); console.log("pwcMatch: ", pwcMatch); code = !paper.codeLink && pwcMatch?.url; note = !paper.note && pwcMatch?.note; diff --git a/src/shared/js/utils/data.js b/src/shared/js/utils/data.js index 3ca2a993..1d05b93f 100644 --- a/src/shared/js/utils/data.js +++ b/src/shared/js/utils/data.js @@ -495,9 +495,9 @@ export const versionToSemantic = (dataVersionInt) => { // 209 -> 0.2.9 // 1293 -> 0.12.93 // 23439 -> 2.34.39 - major = parseInt(dataVersionInt / 1e4, 10); + const major = parseInt(dataVersionInt / 1e4, 10); dataVersionInt -= major * 1e4; - minor = parseInt(dataVersionInt / 1e2, 10); + const minor = parseInt(dataVersionInt / 1e2, 10); dataVersionInt -= minor * 1e2; return `${major}.${minor}.${dataVersionInt}`; }; diff --git a/src/shared/js/utils/parsers.js b/src/shared/js/utils/parsers.js index 894cabfe..dab212b4 100644 --- a/src/shared/js/utils/parsers.js +++ b/src/shared/js/utils/parsers.js @@ -173,11 +173,11 @@ export const extractAuthor = (bibtex) => export const extractCrossrefData = (crossrefResponse) => { if (!crossrefResponse.status || crossrefResponse.status !== "ok") { - error("Cannot parse CrossRef response", crossrefResponse); + logError("Cannot parse CrossRef response", crossrefResponse); return; } if (crossrefResponse["message-type"] !== "work") { - error("Unknown `message-type` from CrossRef", crossrefResponse); + logError("Unknown `message-type` from CrossRef", crossrefResponse); return; } @@ -193,14 +193,14 @@ export const extractCrossrefData = (crossrefResponse) => { : null; if (!year) { - error("Cannot find year in CrossRef data", data); + logError("Cannot find year in CrossRef data", data); return; } const title = data.title[0]; if (!title) { - error("Cannot find title in CrossRef data", data); + logError("Cannot find title in CrossRef data", data); return; } From b4047ad21e5939a580fbac2085ab08b19c26082e Mon Sep 17 00:00:00 2001 From: vict0rsch Date: Mon, 13 Apr 2026 02:27:41 +0200 Subject: [PATCH 04/13] fix: mergePapers addDate logic and contentScriptCallbacks default - Fix inverted addDate comparison in syncMerge: use < instead of > to keep the oldest add date as intended by the comment. - Add done() to default contentScriptCallbacks to prevent TypeError when callers don't provide it. Made-with: Cursor --- src/shared/js/utils/paper.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/shared/js/utils/paper.js b/src/shared/js/utils/paper.js index 62a7e9fd..4d9cef93 100644 --- a/src/shared/js/utils/paper.js +++ b/src/shared/js/utils/paper.js @@ -441,11 +441,9 @@ export const mergePapers = (options = { newPaper: {}, oldPaper: {} }) => { if (newPaper.lastOpenDate > oldPaper.lastOpenDate) { mergedPaper.lastOpenDate = newPaper.lastOpenDate; } - mergedPaper.addDate = newPaper.addDate; // keep oldest add date - if (newPaper.addDate > oldPaper.addDate) { - mergedPaper.addDate = newPaper.addDate; - } + mergedPaper.addDate = + newPaper.addDate < oldPaper.addDate ? newPaper.addDate : oldPaper.addDate; } for (const attribute of opts.overwrites) { if (newPaper.hasOwnProperty(attribute)) { @@ -477,7 +475,12 @@ export const addOrUpdatePaper = async ({ prefs, tab, store = true, - contentScriptCallbacks = { update: () => {}, preprints: () => {}, feedback: null }, + contentScriptCallbacks = { + update: () => {}, + preprints: () => {}, + done: () => {}, + feedback: null, + }, }) => { // start time const aouStart = Date.now(); From 7b58c2ddd36aaa2bac4085e2aa4108d07a4502ff Mon Sep 17 00:00:00 2001 From: vict0rsch Date: Mon, 13 Apr 2026 02:28:02 +0200 Subject: [PATCH 05/13] fix: prepareOverwriteData map destructuring bug Object.entries().map((k, v) => ...) was using the index as v instead of the value. Fix to destructure as ([k, v]) and filter empty arrays. Made-with: Cursor --- src/shared/js/utils/data.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/shared/js/utils/data.js b/src/shared/js/utils/data.js index 1d05b93f..3b04955b 100644 --- a/src/shared/js/utils/data.js +++ b/src/shared/js/utils/data.js @@ -811,11 +811,15 @@ export const prepareOverwriteData = async (data) => { for (const id in papersToWrite) { if (!id.startsWith("__")) { let paperWarnings = validatePaper(papersToWrite[id]).warnings; - if (paperWarnings && paperWarnings.length > 0) { + const hasWarnings = Object.values(paperWarnings).some( + (v) => v.length > 0, + ); + if (hasWarnings) { warning += "
" + Object.entries(paperWarnings) - .map((k, v) => `
${k}
${v.join("
")}`) + .filter(([, v]) => v.length > 0) + .map(([k, v]) => `
${k}
${v.join("
")}`) .join("
"); } } From 89fac3772bfd0bb9840d253ce9ceb02e790ba936 Mon Sep 17 00:00:00 2001 From: vict0rsch Date: Mon, 13 Apr 2026 02:28:23 +0200 Subject: [PATCH 06/13] fix: prevent infinite loop in makeTitle Replace while(1) with a bounded loop (max 20 iterations) to prevent the function from running indefinitely for the lifetime of the tab. Made-with: Cursor --- src/content_scripts/content_script.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/content_scripts/content_script.js b/src/content_scripts/content_script.js index 7f17a38c..2aa458b5 100644 --- a/src/content_scripts/content_script.js +++ b/src/content_scripts/content_script.js @@ -448,7 +448,8 @@ const contentScriptMain = async ({ if (!state.papers.hasOwnProperty(id)) return; const paper = state.papers[id]; const maxWait = 60 * 1000; - while (1) { + const maxIters = 20; + while (PDF_TITLE_ITERS < maxIters) { const waitTime = Math.min(maxWait, 250 * 2 ** PDF_TITLE_ITERS); await sleep(waitTime); document.title = ""; From b02b54fa3bf94e782d125c9a3ce9d244a73ca80a Mon Sep 17 00:00:00 2001 From: vict0rsch Date: Mon, 13 Apr 2026 02:38:16 +0200 Subject: [PATCH 07/13] security: sanitize HTML in content_script.js to prevent XSS - Add escapeHtml utility to functions.js for shared use. - Escape paper.venue and bibtex year in makePaperMemoryHTMLDiv, displayPaperVenue, and huggingfacePapers. - Validate codeLink protocol and escape in displayPaperCode to prevent javascript: URI injection. - Escape paper.id in updateCompleteSecretHTML content attribute. Made-with: Cursor --- src/content_scripts/content_script.js | 19 +++++++++++-------- src/shared/js/utils/functions.js | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/content_scripts/content_script.js b/src/content_scripts/content_script.js index 2aa458b5..04216fb3 100644 --- a/src/content_scripts/content_script.js +++ b/src/content_scripts/content_script.js @@ -19,6 +19,7 @@ import { sendMessageToBackground, downloadURI, dummyEvent, + escapeHtml, } from "@pmu/functions.js"; import { bibtexToString, bibtexToObject } from "@pmu/bibtexParser.js"; import { @@ -254,8 +255,8 @@ const makePaperMemoryHTMLDiv = (paper) => { style="display: flex; justify-content: center; align-items: center;" id="pm-venue" > - ${paper.venue} - ${bibtexToObject(paper.bibtex).year} + ${escapeHtml(paper.venue)} + ${escapeHtml(bibtexToObject(paper.bibtex).year)}

{ const venueDiv = /*html*/ `

- ${paper.venue} - ${bibtexToObject(paper.bibtex).year} + ${escapeHtml(paper.venue)} + ${escapeHtml(bibtexToObject(paper.bibtex).year)}
`; findEl({ element: "pm-publication-wrapper" })?.remove(); @@ -628,9 +629,11 @@ const displayPaperCode = (paper) => { if (!paper.codeLink) { return; } + const safeLink = /^https?:\/\//.test(paper.codeLink) ? escapeHtml(paper.codeLink) : ""; + if (!safeLink) return; const code = /*html*/ `
-

Code:

${paper.codeLink} +

Code:

${safeLink}
`; findEl({ element: "pm-code" })?.remove(); @@ -649,8 +652,8 @@ const huggingfacePapers = (paper, url) => { const venueDiv = /*html*/ `
- ${paper.venue} - ${bibtexToObject(paper.bibtex).year} + ${escapeHtml(paper.venue)} + ${escapeHtml(bibtexToObject(paper.bibtex).year)} (PaperMemory)
`; @@ -853,7 +856,7 @@ const updateCompleteSecretHTML = (paper) => { .querySelector("head") .insertAdjacentHTML( "beforeend", - /*html*/ ``, + /*html*/ ``, ); } }, 50); diff --git a/src/shared/js/utils/functions.js b/src/shared/js/utils/functions.js index d98459a6..51592751 100644 --- a/src/shared/js/utils/functions.js +++ b/src/shared/js/utils/functions.js @@ -9,6 +9,21 @@ import { import { LOGTRACE } from "@pmu/logTrace.js"; import { val, hasClass, findEl } from "@pmu/miniquery.js"; +/** + * Escapes HTML special characters to prevent XSS when inserting into HTML. + * @param {string} str The string to escape + * @returns {string} The escaped string + */ +export const escapeHtml = (str) => { + if (typeof str !== "string") return String(str ?? ""); + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +}; + /** * Generate a random integer between 0 and max * @param {number} max The maximum value of the random integer From 68de4bf459777c2cd71ee736087428af387242ff Mon Sep 17 00:00:00 2001 From: vict0rsch Date: Mon, 13 Apr 2026 02:40:52 +0200 Subject: [PATCH 08/13] security: sanitize HTML in popup templates and options page - templates.js: escape paper.title, note, codeLink, author, and tag names before HTML insertion in getMemoryItemHTML and getPopupEditFormHTML. - options.js: escape title/authors in getAutoTagHTML value attributes, paper.title in addPreprintUpdate, URLs in import feedback, update content values in startMatching, and error objects in sync feedback. Made-with: Cursor --- src/options/options.js | 23 ++++++++++++----------- src/popup/js/templates.js | 22 +++++++++++----------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/options/options.js b/src/options/options.js index 805bc1c2..b6e9f429 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -32,6 +32,7 @@ import { cleanPapers, sendMessageToBackground, parseTags, + escapeHtml, } from "@pmu/functions.js"; import { makePaper, @@ -248,7 +249,7 @@ const handleParseImportJson = async (e) => { if (!Object.values(is).some((i) => i)) { feedback.innerHTML += `
  • [${ i + 1 - }]  ×  Error: ${url} does not come from a known source
  • `; + }]  ×  Error: ${escapeHtml(url)} does not come from a known source`; warn("Aborting."); } else { let paper; @@ -266,12 +267,12 @@ const handleParseImportJson = async (e) => { if (exists) { feedback.innerHTML += `
  • [${ i + 1 - }]  ×  Warning: ${url} already exists and has been ignored
  • `; + }]  ×  Warning: ${escapeHtml(url)} already exists and has been ignored`; warn("Aborting."); } else { feedback.innerHTML += `
  • [${ i + 1 - }]  ✔ ${url} has been successfully added to your Memory!
  • `; + }]  ✔ ${escapeHtml(url)} has been successfully added to your Memory!`; } } } catch (error) { @@ -279,7 +280,7 @@ const handleParseImportJson = async (e) => { warn("Aborting."); feedback.innerHTML += `
  • [${ i + 1 - }]  ×  Error: ${url} (open the JavaScript Console for more info)
  • `; + }]  ×  Error: ${escapeHtml(url)} (open the JavaScript Console for more info)`; } } await pushToRemote(); @@ -348,13 +349,13 @@ const getAutoTagHTML = (at) => { return /*html*/ `
    - +
    - +
    - +
    @@ -496,14 +497,14 @@ const addPreprintUpdate = (update) => { for (const [k, v] of Object.entries(update)) { if (k !== "paper" && k !== "bibtex") { if (v) { - contents.push(`${k}: ${v}`); + contents.push(`${escapeHtml(k)}: ${escapeHtml(v)}`); } } } contents = contents.join("
    "); const html = /*html*/ `
    -

    ${paper.title}

    +

    ${escapeHtml(paper.title)}

    Updates to approve:
    @@ -555,7 +556,7 @@ const startMatching = async (papersToMatch) => { for (const [idx, paper] of papersToMatch.entries()) { console.log("idx: ", idx); setHTML("matching-status-index", idx + 1); - setHTML("matching-status-title", paper.title); + setHTML("matching-status-title", escapeHtml(paper.title)); changeProgress(parseInt((idx / papersToMatch.length) * 100)); var bibtex, venue, note, code, match; @@ -1092,7 +1093,7 @@ const setupSync = async () => { } } } catch (e) { - setHTML("overwriteRemoteFeedback", e); + setHTML("overwriteRemoteFeedback", escapeHtml(String(e))); } await sendMessageToBackground({ type: "restartGist" }); hideId("sync-loader"); diff --git a/src/popup/js/templates.js b/src/popup/js/templates.js index 839d4df2..6eb87c95 100644 --- a/src/popup/js/templates.js +++ b/src/popup/js/templates.js @@ -1,6 +1,6 @@ // ES Module imports import { state, knownPaperPages, svgActionsHoverTitles } from "@pmu/config.js"; -import { getDisplayId, tablerSvg, isPdfUrl, cutAuthors } from "@pmu/functions.js"; +import { getDisplayId, tablerSvg, isPdfUrl, cutAuthors, escapeHtml } from "@pmu/functions.js"; import { getTagsOptions } from "@pmu/state.js"; import { isPaper } from "@pmu/paper.js"; /** @@ -53,7 +53,7 @@ export const getMemoryItemHTML = (paper) => { const favoriteClass = paper.favorite ? "favorite" : ""; const titles = { ...svgActionsHoverTitles }; // titles behave differently in build/watch mode. This works in build - titles.pdfLink = `Open tab to ${paper.title}`; + titles.pdfLink = `Open tab to ${escapeHtml(paper.title)}`; titles.copyLink = `Copy URL to the paper's ${ state.prefs.checkPreferPdf ? "PDF" : "abstract" }`; @@ -62,7 +62,7 @@ export const getMemoryItemHTML = (paper) => {
    ${ @@ -78,7 +78,7 @@ export const getMemoryItemHTML = (paper) => { noteDiv = /*html*/ `
    Note: - ${note} + ${escapeHtml(note)}
    `; } @@ -163,7 +163,7 @@ export const getMemoryItemHTML = (paper) => { favoriteClass, ])} - ${paper.title} + ${escapeHtml(paper.title)} @@ -171,7 +171,7 @@ export const getMemoryItemHTML = (paper) => {
    ${[...tags] - .map((t) => `${t}`) + .map((t) => `${escapeHtml(t)}`) .join("")}
    - ${cutAuthors(paper.author)} + ${cutAuthors(escapeHtml(paper.author))}
    ${codeDiv} ${noteDiv}
    @@ -256,7 +256,7 @@ export const getMemoryItemHTML = (paper) => {
    @@ -267,7 +267,7 @@ export const getMemoryItemHTML = (paper) => { class="form-note-textarea" placeholder="Anything to note?" > -${note}
    @@ -324,7 +324,7 @@ export const getPopupEditFormHTML = (paper) => { id="popup-form-codeLink--${id}" type="text" class="form-code-input mt-0" - value="${paper.codeLink || ""}" + value="${escapeHtml(paper.codeLink || "")}" placeholder="Add code link" />
    @@ -336,7 +336,7 @@ export const getPopupEditFormHTML = (paper) => { id="popup-form-note-textarea--${id}" placeholder="Anything to note?" > -${note}
    From f31d2a4f0a06fba0716b3f3b94cd2d41c7059b75 Mon Sep 17 00:00:00 2001 From: vict0rsch Date: Mon, 13 Apr 2026 02:42:21 +0200 Subject: [PATCH 09/13] security: sanitize HTML in functions.js, data.js, and bibMatcher.js - functions.js: escape URL and title in copyHyperLinkToClipboard, escape error stack lines in stringifyError. - data.js: escape validation warning keys and values in prepareOverwriteData. - bibMatcher.js: escape paper titles, citation keys, venues, and sources in updateMatchedTitles and matchBibliography status output. Made-with: Cursor --- src/bibMatcher/bibMatcher.js | 12 ++++++------ src/shared/js/utils/data.js | 3 ++- src/shared/js/utils/functions.js | 12 +++++++----- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/bibMatcher/bibMatcher.js b/src/bibMatcher/bibMatcher.js index 7c0e9be1..84fca100 100644 --- a/src/bibMatcher/bibMatcher.js +++ b/src/bibMatcher/bibMatcher.js @@ -9,7 +9,7 @@ import { hideId, querySelector, } from "@pmu/miniquery.js"; -import { copyTextToClipboard } from "@pmu/functions.js"; +import { copyTextToClipboard, escapeHtml } from "@pmu/functions.js"; import { BibtexParser, bibtexToObject, bibtexToString } from "@pmu/bibtexParser.js"; import { tryDBLP, @@ -313,7 +313,7 @@ const matchItems = async (papersToMatch) => { setHTML("matching-status-index", idx + 1); setHTML( "matching-status-title", - paper.title.replaceAll("{", "").replaceAll("}", ""), + escapeHtml(paper.title.replaceAll("{", "").replaceAll("}", "")), ); changeProgress(parseInt((idx / papersToMatch.length) * 100)); @@ -365,10 +365,10 @@ const updateMatchedTitles = (matchedBibtexStrs, sources, venues) => { for (const [idx, title] of titles.entries()) { htmls.push( ` - ${keys[idx]} - ${title} - ${venues[idx]} - ${sources[idx]} + ${escapeHtml(keys[idx])} + ${escapeHtml(title)} + ${escapeHtml(venues[idx])} + ${escapeHtml(sources[idx])} `, ); } diff --git a/src/shared/js/utils/data.js b/src/shared/js/utils/data.js index 3b04955b..28eb7ace 100644 --- a/src/shared/js/utils/data.js +++ b/src/shared/js/utils/data.js @@ -11,6 +11,7 @@ import { miniHash, parseUrl, stringifyError, + escapeHtml, } from "@pmu/functions.js"; import { state, @@ -819,7 +820,7 @@ export const prepareOverwriteData = async (data) => { "
    " + Object.entries(paperWarnings) .filter(([, v]) => v.length > 0) - .map(([k, v]) => `
    ${k}
    ${v.join("
    ")}`) + .map(([k, v]) => `
    ${escapeHtml(k)}
    ${v.map(escapeHtml).join("
    ")}`) .join("
    "); } } diff --git a/src/shared/js/utils/functions.js b/src/shared/js/utils/functions.js index 51592751..9ec65b82 100644 --- a/src/shared/js/utils/functions.js +++ b/src/shared/js/utils/functions.js @@ -372,7 +372,7 @@ async function pasteRich(rich, plain) { * @returns {void} * */ export const copyHyperLinkToClipboard = (url, title) => { - const linkHtml = `${title}`; + const linkHtml = `${escapeHtml(title)}`; pasteRich(linkHtml, `${title} ${url}`); }; @@ -801,10 +801,12 @@ export const stringifyError = (e) => { return e.stack .split("\n") .map((line) => - line - .split(" ") - .map((word) => word.split(extId).last()) - .join(" "), + escapeHtml( + line + .split(" ") + .map((word) => word.split(extId).last()) + .join(" "), + ), ) .join("
    "); }; From 25957889455b4b1a49a1a3b3ae3a0a9f6c10414f Mon Sep 17 00:00:00 2001 From: vict0rsch Date: Mon, 13 Apr 2026 02:43:04 +0200 Subject: [PATCH 10/13] security: add sender validation and download sanitization in background.js - Validate sender.id matches extension ID on all messages to reject messages from external extensions/web pages. - Improve filename sanitization to strip all filesystem-unsafe chars, collapse consecutive dots, and limit length. - Validate pdfUrl protocol before initiating download. Made-with: Cursor --- src/background/background.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/background/background.js b/src/background/background.js index 29701b97..91dd14c3 100755 --- a/src/background/background.js +++ b/src/background/background.js @@ -337,6 +337,9 @@ export function initBackground() { }; chrome.runtime.onMessage.addListener((payload, sender, sendResponse) => { + if (sender.id !== chrome.runtime.id) { + return; + } if (payload.type === "update-title") { const { title, url } = payload.options; paperTitles[url] = title.replaceAll('"', "'"); @@ -357,12 +360,15 @@ export function initBackground() { }); } const safeTitle = payload.title - .replaceAll("?", "") - .replaceAll(":", "") - .replaceAll("..", "") - .replaceAll("/", "_") - .replaceAll("\\", "_"); + .replace(/[<>:"/\\|?*\x00-\x1f]/g, "_") + .replace(/\.{2,}/g, ".") + .replace(/^\.+|\.+$/g, "") + .slice(0, 200); const filename = "PaperMemoryStore/" + safeTitle; + if (!/^https?:\/\//.test(payload.pdfUrl)) { + sendResponse(false); + return; + } chrome.downloads.download({ url: payload.pdfUrl, filename }); sendResponse(true); }); From 9604ea6baa92b55f3e1f8068934696d98fd237f6 Mon Sep 17 00:00:00 2001 From: vict0rsch Date: Mon, 13 Apr 2026 02:44:10 +0200 Subject: [PATCH 11/13] security: fix PAT logging and autoTag ReDoS - Redact GitHub PAT from console log output in options.js. - Replace regex-based autoTag matching with case-insensitive string includes to eliminate ReDoS risk from user-supplied patterns. Made-with: Cursor --- src/options/options.js | 2 +- src/shared/js/utils/parsers.js | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/options/options.js b/src/options/options.js index b6e9f429..7cb3ed41 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -949,7 +949,7 @@ const setupSync = async () => { const { file, pat, gistId } = payload; log("Gist ID", gistId); log("Data URL", file.raw_url); - log("Personal Access Token", pat); + log("Personal Access Token", "[REDACTED]"); setHTML("pat-feedback", "Ok! Token is valid."); toggleSync(); } diff --git a/src/shared/js/utils/parsers.js b/src/shared/js/utils/parsers.js index dab212b4..599665f4 100644 --- a/src/shared/js/utils/parsers.js +++ b/src/shared/js/utils/parsers.js @@ -1924,12 +1924,22 @@ export const autoTagPaper = async (paper) => { if (!at.tags?.length) continue; if (!at.title && !at.author) continue; - const titleMatch = at.title - ? new RegExp(at.title, "i").test(paper.title) - : true; - const authorMatch = at.author - ? new RegExp(at.author, "i").test(paper.author) - : true; + let titleMatch = true; + let authorMatch = true; + try { + if (at.title) { + titleMatch = paper.title + .toLowerCase() + .includes(at.title.toLowerCase()); + } + if (at.author) { + authorMatch = paper.author + .toLowerCase() + .includes(at.author.toLowerCase()); + } + } catch (e) { + continue; + } if (titleMatch && authorMatch) { at.tags.forEach((t) => tags.add(t)); From b0e35c58ccc82e7d50e6b6c6586f3c346bcd1a0f Mon Sep 17 00:00:00 2001 From: vict0rsch Date: Mon, 13 Apr 2026 12:11:50 +0200 Subject: [PATCH 12/13] fix: address code review and security review findings Made-with: Cursor --- src/background/background.js | 8 ++++++-- src/content_scripts/content_script.js | 9 +++++---- src/options/options.js | 4 ++-- src/popup/js/templates.js | 2 +- src/shared/js/utils/parsers.js | 8 ++------ 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/background/background.js b/src/background/background.js index 91dd14c3..8715f6c4 100755 --- a/src/background/background.js +++ b/src/background/background.js @@ -87,7 +87,9 @@ export function initBackground() { // Remove DOM-dependent setFaviconCode since it's not needed in service worker const fetchOpenReviewNoteJSON = async (url) => { - const id = url.match(/id=([\w-]+)/)[1]; + const match = url.match(/id=([\w-]+)/); + if (!match) return; + const id = match[1]; const api = `https://api.openreview.net/notes?id=${id}`; let response = await fetch(api); let json = await response.json(); @@ -101,7 +103,9 @@ export function initBackground() { }; const fetchOpenReviewForumJSON = async (url) => { - const id = url.match(/id=([\w-]+)/)[1]; + const match = url.match(/id=([\w-]+)/); + if (!match) return; + const id = match[1]; const api = `https://api.openreview.net/notes?forum=${id}`; let response = await fetch(api); let json = await response.json(); diff --git a/src/content_scripts/content_script.js b/src/content_scripts/content_script.js index 04216fb3..65029327 100644 --- a/src/content_scripts/content_script.js +++ b/src/content_scripts/content_script.js @@ -136,7 +136,6 @@ $.extend($.easing, { }, }); -var PDF_TITLE_ITERS = 0; /** * Centralizes HTML svg codes @@ -450,12 +449,13 @@ const contentScriptMain = async ({ const paper = state.papers[id]; const maxWait = 60 * 1000; const maxIters = 20; - while (PDF_TITLE_ITERS < maxIters) { - const waitTime = Math.min(maxWait, 250 * 2 ** PDF_TITLE_ITERS); + let pdfTitleIters = 0; + while (pdfTitleIters < maxIters) { + const waitTime = Math.min(maxWait, 250 * 2 ** pdfTitleIters); await sleep(waitTime); document.title = ""; document.title = paper.title; - PDF_TITLE_ITERS++; + pdfTitleIters++; } }; makeTitle(id); @@ -660,6 +660,7 @@ const huggingfacePapers = (paper, url) => { const abstractH2 = queryAll("h2").find((h) => h.innerText.trim() === "Abstract"); if (!abstractH2) { log("Missing 'Abstract' h2 title on HuggingFace paper page."); + return; } const authorDiv = abstractH2.parentElement.previousElementSibling; log("Adding venue to HuggingFace paper page."); diff --git a/src/options/options.js b/src/options/options.js index 7cb3ed41..1dea8bf0 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -919,7 +919,7 @@ const setupSync = async () => { if (!ok) { if (error) { - setHTML("pat-feedback", "Invalid PAT" + "

    " + error); + setHTML("pat-feedback", "Invalid PAT" + "

    " + escapeHtml(String(error))); } hideId("pat-loader"); await toggleSync({ hideAll: true }); @@ -944,7 +944,7 @@ const setupSync = async () => { const { ok, payload, error } = await getGist({ pat }); if (!ok) { logError(error); - setHTML("pat-feedback", error.response.data.message); + setHTML("pat-feedback", escapeHtml(String(error.response.data.message))); } else { const { file, pat, gistId } = payload; log("Gist ID", gistId); diff --git a/src/popup/js/templates.js b/src/popup/js/templates.js index 6eb87c95..12e4ff22 100644 --- a/src/popup/js/templates.js +++ b/src/popup/js/templates.js @@ -21,7 +21,7 @@ export const getPaperInfoTable = (paper) => { if (paper.venue) tableData.push([ "Publication", - `${paper.venue} ${paper.year}`, + `${escapeHtml(paper.venue)} ${escapeHtml(String(paper.year))}`, ]); return /*html*/ ` diff --git a/src/shared/js/utils/parsers.js b/src/shared/js/utils/parsers.js index 599665f4..b52dc451 100644 --- a/src/shared/js/utils/parsers.js +++ b/src/shared/js/utils/parsers.js @@ -1928,14 +1928,10 @@ export const autoTagPaper = async (paper) => { let authorMatch = true; try { if (at.title) { - titleMatch = paper.title - .toLowerCase() - .includes(at.title.toLowerCase()); + titleMatch = new RegExp(at.title, "i").test(paper.title); } if (at.author) { - authorMatch = paper.author - .toLowerCase() - .includes(at.author.toLowerCase()); + authorMatch = new RegExp(at.author, "i").test(paper.author); } } catch (e) { continue; From 9794d15386022c699d8a13a8909c9a640e2a0059 Mon Sep 17 00:00:00 2001 From: vict0rsch Date: Mon, 13 Apr 2026 12:37:52 +0200 Subject: [PATCH 13/13] ci: add build step before running tests The WXT migration removed pre-committed bundles, so dist/chrome-mv3 must be built before Puppeteer can load the extension. Made-with: Cursor --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d03e9d43..16346a82 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,6 +37,9 @@ jobs: - name: Install Dependencies run: npm ci + - name: Build Extension + run: npm run build:chrome + - uses: nick-fields/retry@v3 name: Run Test with: @@ -59,6 +62,9 @@ jobs: - name: Install Dependencies run: npm ci + - name: Build Extension + run: npm run build:chrome + - uses: nick-fields/retry@v3 name: Run Storage Test with: