Is Notepal - 微信读书笔记同步助手 safe?
Notepal sends WeRead note and highlight contents to Mixpanel when sync errors occur.
When Notepal syncs WeRead notes to supported services, its error handling can include the note data in a Mixpanel event. That data includes highlight text, note text, book titles, authors, chapter titles, cover image URLs, source URLs, and creation times.
Who publishes itRandynamic, LLC - 2 other listings from the same operator, none carrying a finding
Randynamic, LLC - 2 other listings from the same operator, none carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
2 other listings published from this account, 3k+ users between them, none of them carrying a finding.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
WeRead sync errors send note contents to Mixpanel
When a WeRead sync destination errors, the extension attaches note records to a Mixpanel Error event.
A POST to api.mixpanel.com/track carried properties.NoteDatas: highlight text, notes, book title, author, cover URL, chapter title.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You start syncing WeRead notes to Siyuan, Readwise, or Logseq.
The issue appears when that destination returns an error after the extension has prepared the note data.
The extension sends a Mixpanel error event with the prepared note records attached.
Those records are placed under the NoteDatas property before the tracker serializes the event.
- Your highlighted texthighlightText: The margin notes changed how I read this chapter (illustrative)
The highlighted passage can reveal what you selected while reading.
- Your written notenoteText: Follow up with the team about this idea (illustrative)
A review note can contain your own commentary, names, or other personal context.
- Book and authorbookTitle: Atomic Habits; author: James Clear (illustrative)
The book metadata connects the note to the title and author you were reading.
- WeRead source linkhttps://weread.qq.com/wrpage/book/share/3300024248 (illustrative)
The share link points back to the WeRead book page associated with the note.
- Creation timecreateTime: 1710950400 (illustrative)
The timestamp places the note in time and can help reconstruct your reading activity.
Observed during dynamic analysis after a forced sync-destination error.
- Content-Type
- application/json
The shipped sync code passes note records into Mixpanel error telemetry
Readable equivalent: create noteDatas from WeRead highlights and reviews
tabs/sync.4d579049.jsfunction WeReadHighlightToNoteDatas(bookmarkList) { return bookmarkList.updated.map((highlight) => { const chapterTitle = bookmarkList.chapters.find((chapter) => chapter.chapterUid === highlight.chapterUid)?.title; return { highlightText: highlight.markText, bookTitle: bookmarkList.book.title, author: bookmarkList.book.author, chapterTitle, imageUrl: bookmarkList.book.cover, sourceUrl: `https://weread.qq.com/wrpage/book/share/${highlight.bookId}`, createTime: highlight.createTime }; });}function WeReadReviewToNoteDatas(reviewList) { return reviewList.reviews .map((item) => ({ highlightText: item.review.abstract, bookTitle: item.review.book.title, author: item.review.book.author, imageUrl: item.review.book.cover, noteText: item.review.content, chapterTitle: item.review.chapterTitle, sourceUrl: `https://weread.qq.com/wrpage/book/share/${item.review.bookId}`, createTime: item.review.createTime })) .filter((noteData) => noteData.highlightText);}async function getWereadNotes(bookId) { const highlights = mockLog ? mockLog.highlights : await getWeReadHighlights(bookId); const notes = mockLog ? mockLog.notes : await getWeReadNotes(bookId); if (localStorage.getItem(StorageKey.DebugMode) === "true") { await getWeReadNotebooks(); navigator.userAgent; } const highlightNoteDatas = WeReadHighlightToNoteDatas(highlights); const reviewNoteDatas = WeReadReviewToNoteDatas(notes); return { noteDatas: highlightNoteDatas.concat(reviewNoteDatas).sort((left, right) => (left.createTime || 0) - (right.createTime || 0)), highlights, notes };}Readable equivalent: destination error paths include NoteDatas
tabs/sync.4d579049.jsasync function syncSiyuan(noteDatas) { const notebookId = await storage.get(StorageKey.SiyuanNotebookId); if (!notebookId) throw Error("Set the Siyuan notebook id first"); const template = (await storage.get(StorageKey.SiyuanTemplate)) || defaultSiyuanTemplate; const syncPathTemplate = (await storage.get(StorageKey.SiyuanSyncPath)) || "/books/{{bookTitle}}"; const groupedByChapter = groupBy(noteDatas, "chapterTitle"); const bookTitle = noteDatas[0].bookTitle; const path = await liquid.parseAndRender(syncPathTemplate, { bookTitle }); try { const markdown = await liquid.parseAndRender(template, { notes: groupedByChapter, ...noteDatas[0] }); const response = await postSiyuan("/api/filetree/createDocWithMd", { notebook: notebookId, path, markdown }); const body = await response.json(); if (response.ok) { trackEvent([{ event: EventType.SyncSiyuan, properties: { Highlights_count: noteDatas.length } }]); await storage.set(StorageKey.ExportCount, ((await storage.get(StorageKey.ExportCount)) || 0) + noteDatas.length); return { success: true, url: `siyuan://blocks/${body.data}` }; } throw Error(body.msg); } catch (error) { trackEvent([{ event: EventType.Error, properties: { Type: EventType.SyncSiyuan, Error: error, NoteDatas: noteDatas } }]); throw Error("Sync failed"); }}async function syncReadwise(noteDatas) { const highlights = noteDatas.map(noteDataToReadwiseHighlight).filter((highlight) => highlight.text && highlight.text.length > 0); const token = await storage.get(StorageKey.ReadwiseToken); if (!token) throw Error("Readwise Token not set"); const valid = await validateReadwiseToken(token); if (!valid) throw Error("Readwise Token invalid"); const response = await fetch("https://readwise.io/api/v2/highlights", { method: "POST", body: JSON.stringify({ highlights }), headers: new Headers({ "Content-Type": "application/json", Authorization: `Token ${token}` }) }); const body = await response.json(); if (response.status < 200 || response.status >= 300) { trackEvent([{ event: EventType.Error, properties: { Type: "HTTP", Status: response.status, Response: body, NoteDatas: highlights } }]); } else { await storage.set(StorageKey.ExportCount, ((await storage.get(StorageKey.ExportCount)) || 0) + highlights.length); trackEvent([{ event: EventType.SyncReadwise, properties: { Highlights_count: highlights.length } }]); } return { status: response.status, data: body };}async function syncLogseq(noteDatas) { const connected = await checkLogseqConnect(); if (!connected) throw Error("Logseq connection failed; check the token and API server"); const groupedByChapter = groupBy(noteDatas, "chapterTitle"); const bookTitle = noteDatas[0].bookTitle; const pageTemplate = await storage.get(StorageKey.LogseqSyncPageName); const blockTemplate = (await storage.get(StorageKey.LogseqNoteBlockTemplate)) || defaultLogseqNoteBlockTemplate; const pageName = await liquid.parseAndRender(pageTemplate, { bookTitle }); try { const pageResponse = await callLogseq("logseq.Editor.createPage", [pageName, {}, {}]); const { uuid } = await pageResponse.json(); const blocks = Object.keys(groupedByChapter).map((chapterTitle) => ({ content: `${chapterTitle}`, children: groupedByChapter[chapterTitle].map((noteData) => ({ content: liquid.parseAndRenderSync(blockTemplate, { chapterTitle, bookTitle, highlightText: noteData.highlightText, noteText: noteData.noteText, author: noteData.author }) })) })); await callLogseq("logseq.Editor.insertBatchBlock", [uuid, blocks]); await storage.set(StorageKey.ExportCount, ((await storage.get(StorageKey.ExportCount)) || 0) + noteDatas.length); trackEvent([{ event: EventType.SyncLogseq, properties: { Highlights_count: noteDatas.length } }]); return { url: `logseq://graph/logseq?page=${pageName}` }; } catch (error) { trackEvent([{ event: EventType.Error, properties: { Type: EventType.SyncLogseq, Error: error, NoteDatas: noteDatas } }]); throw error; }}Readable equivalent: trackEvent posts the complete event array to Mixpanel
tabs/sync.4d579049.jsasync function trackEvent(events) { const debugId = await storage.get(StorageKey.DebugId); events.forEach((event) => { event.properties ||= {}; event.properties.Version = packageJson.version; event.properties.time = Number(new Date()); event.properties.token = "77cf4f0a97fe05780b95175c9eac4765"; event.properties.distinct_id = debugId; }); const response = await fetch("https://api.mixpanel.com/track?strict=1", { method: "POST", headers: new Headers({ "Content-Type": "application/json" }), body: JSON.stringify(events) }); return response;}- weread.qq.com
Source host for notebook, highlight, and review data loaded with the user's WeRead session.
- api.mixpanel.com
Mixpanel tracking endpoint that receives the Error event containing NoteDatas.
Where it sends data
Destinations our analysis observed Notepal - 微信读书笔记同步助手 contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- api.mixpanel.comwidely used
Notepal - 微信读书笔记同步助手 sends data to api.mixpanel.com. A widely used service: 35 other extensions we have analysed send data here.