From ee68452b0519686f65446c63497038676efea77e Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Mon, 20 Jul 2026 23:25:21 -0400 Subject: [PATCH] Disallow slashes in stored-file attachment paths Someone ended up (via a plugin, presumably) with stored-file attachments with a full path after 'storage:', which broke file syncing. Throw when setting a stored-file path containing a slash, and strip paths from existing filenames in a schema update step. No particular reason to think that the file with that basename will exist in the storage dir, but at least it will be looking for the right file and not be totally broken. Separately, the dataserver will clean up filenames with full paths and block going forward. https://forums.zotero.org/discussion/132822/reference-sychronization-error --- chrome/content/zotero/xpcom/data/item.js | 3 +++ chrome/content/zotero/xpcom/schema.js | 13 +++++++++++++ resource/schema/userdata.sql | 2 +- test/tests/itemTest.js | 12 +++++++++++- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js index ed89ab3cf4..014934fff9 100644 --- a/chrome/content/zotero/xpcom/data/item.js +++ b/chrome/content/zotero/xpcom/data/item.js @@ -3663,6 +3663,9 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentPath', { } val = 'storage:' + PathUtils.filename(val); } + if (/^storage:.*[/\\]/.test(val)) { + throw new Error(`Stored file filename cannot contain a slash -- got '${val}'`); + } } if (val == this.attachmentPath) { diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js index 8803b1d200..1b5872baeb 100644 --- a/chrome/content/zotero/xpcom/schema.js +++ b/chrome/content/zotero/xpcom/schema.js @@ -3690,6 +3690,19 @@ Zotero.Schema = new function () { await Zotero.DB.queryAsync("INSERT INTO savedSearchConditions SELECT savedSearchID, searchConditionID, condition, operator, value FROM savedSearchConditionsOld"); await Zotero.DB.queryAsync("DROP TABLE savedSearchConditionsOld"); } + + else if (i == 128) { + // Strip full paths (e.g., from third-party tools) from stored-file attachment + // paths, which should contain only a filename after 'storage:' + let rows = await Zotero.DB.queryAsync("SELECT itemID, path FROM itemAttachments WHERE linkMode IN (0, 1) AND (path LIKE ? OR path LIKE ?)", ['storage:%/%', 'storage:%\\%']); + for (let row of rows) { + let filename = row.path.substr(8).split(/[/\\]/).pop(); + if (!filename) { + continue; + } + await Zotero.DB.queryAsync("UPDATE itemAttachments SET path=? WHERE itemID=?", ['storage:' + filename, row.itemID]); + } + } } await _updateDBVersion('userdata', toVersion); diff --git a/resource/schema/userdata.sql b/resource/schema/userdata.sql index cc25b7c841..c0d5724a31 100644 --- a/resource/schema/userdata.sql +++ b/resource/schema/userdata.sql @@ -1,4 +1,4 @@ --- 127 +-- 128 -- Copyright (c) 2009 Center for History and New Media -- George Mason University, Fairfax, Virginia, USA diff --git a/test/tests/itemTest.js b/test/tests/itemTest.js index 4589179370..32afa28f60 100644 --- a/test/tests/itemTest.js +++ b/test/tests/itemTest.js @@ -1158,7 +1158,17 @@ describe("Zotero.Item", function () { assert.equal(attachment.attachmentFilename, filename); }); - + + it("should reject a filename containing a slash", async function () { + var item = await createDataObject('item'); + + var attachment = new Zotero.Item("attachment"); + attachment.attachmentLinkMode = Zotero.Attachments.LINK_MODE_IMPORTED_FILE; + attachment.parentID = item.id; + assert.throws(() => attachment.attachmentFilename = "D:/Foo/Bar/test.pdf", /slash/); + assert.throws(() => attachment.attachmentFilename = "D:\\Foo\\Bar\\test.pdf", /slash/); + }); + it("should get a filename for a base-dir-relative file", function () { var dir = getTestDataDirectory().path; Zotero.Prefs.set('saveRelativeAttachmentPath', true)