Is Xiaohongshu for Desktop safe?

Medium risk

Xiaohongshu for Desktop sends each post's ID and Xiaohongshu access token to a third-party server (api.old-panda.com) to fetch media.

When you download a Xiaohongshu post, the extension reads the post ID and the post's xsec_token (Xiaohongshu's own per-resource signed access token) from the page URL and forwards both to api.old-panda.com, which returns the post's image and video URLs, title and text. Separately, its translate buttons send the visible text of a post or a selected comment to Google Translate's public endpoint. Both happen only when you click download or translate, not in the background.

Seiriv1.2.9Chrome Web Store
45Risk
Who publishes it

Seiri - 6 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.

Publisher
Seiri

Same store account

6 other listings published from this account, 30k+ users between them, none of them carrying a finding.

Shared hosts - 1 hostname

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

api.old-panda.com
Also called by 3 other listings, including Douban Book+, 小地瓜 —— 小红书图片视频下载助手

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Download action sends Xiaohongshu token to old-panda API

MediumunwantedCWE-200Dynamic sandbox

Clicking the download button makes the content script read the post ID and xsec_token from the page URL and send them to the worker, which GETs api.old-panda.com for the media, title, and text.

What actually happens

You did this

You click the download button that the extension adds to a Xiaohongshu or Rednote post.

The extension did this

The extension reads the post ID and page token from the URL and sends them to api.old-panda.com.

Values forwarded for the download lookup

  • Post identifier
    65f1a2b3c4d5e6f7890abcde

    This identifies the exact Xiaohongshu or Rednote post you chose to download.

  • Post access token
    AT_eyJzaWQiOiJ4aHMiLCJub25jZSI6IjE3MTYyNjM0MDAifQ

    This token is copied from the page URL and travels with the post lookup request.

  • Returned media URLs
    https://sns-img-qc.xhscdn.com/1040g00830vtb9apg6g005n4bq6c0j6a2tq8h6f8

    The response can describe the images or videos that will be saved from that post.

  • Returned title and text
    Weekend route notes Coffee shops and photo locations near the river

    The response can include the title and written content of the post you selected.

Captured request

GEThttps://api.old-panda.com/image/xiaohongshu/?post_id=65f1a2b3c4d5e6f7890abcde&xsec_token=AT_eyJzaWQiOiJ4aHMiLCJub25jZSI6IjE3MTYyNjM0MDAifQ

The service worker expects 200 OK JSON and forwards the parsed response to the content script.

Headers
Content-Type
application/json

The code that does this

The shipped code parses the token and forwards it to the API

Readable version

Readable equivalent of the content-script parser

assets/js/content.js
  async downloadPostByHref(href) {    if (!href.includes(XHS_DOMAIN_ZH) && !href.includes(XHS_DOMAIN_EN)) {      throw new Error("Unsupported post URL.");    }    const url = new URL(href);    const postId = url.pathname      .split("/")      .filter((e) => e)      .pop();    const xsecToken = url.searchParams.get("xsec_token") || "";    const response = await browser.runtime.sendMessage({      action: ACTION_GET_DL_DATA,      postId,      xsecToken,    });    if (!response?.success) {      throw new Error(        getErrorMessage(response?.message, "Failed to fetch post data."),      );    }    const storageData = await getStorage([STORAGE_FILENAME_FORMAT]);    const fileNameFormat = storageData[STORAGE_FILENAME_FORMAT];    const { baseName, zipBlob } = await zipFile(      response.data,      postId,      fileNameFormat,    );    const fileName = `${baseName}${ZIP_FILE_NAME_SUFFIX}`;    this.saveBlob(zipBlob, fileName);    return fileName;  }

Readable equivalent of the service-worker request handler

assets/js/background.js
  async handleMessage(message, sendResponse, Filter) {    if (message.action === ACTION_GET_DL_DATA) {      try {        const response = await fetch(          `${DL_API_URL}${message.postId}&xsec_token=${message.xsecToken}`,          {            headers: { "Content-Type": "application/json" },          },        );        if (response.status == 200) {          const data = await response.json();          sendResponse({ success: true, data });        } else {          throw new Error(            "Failed to get video information. Please try again later or check your network connection.",          );        }      } catch (error) {        sendResponse({ success: false, message: error.message });      }    } else if (message.action === ACTION_GET_TRANSLATE_DATA) {      const currentLang = await getStorage([STORAGE_TRANSLATE_LANGUAGE]);      let lang =        currentLang[STORAGE_TRANSLATE_LANGUAGE] || DEFAULT_TRANSLATE_LANG;      if (lang === DEFAULT_TRANSLATE_LANG) {        lang = browser.i18n.getUILanguage();      }      try {        const data = await this.translateText(          message.text,          message.tagText,          lang,        );        sendResponse({ success: true, data });      } catch (error) {        sendResponse({ success: false, message: error.message });      }    } else if (message.action === ACTION_OPEN_SETTING) {      await browser.tabs.create({        url: browser.runtime.getURL(SETTING_PAGE),      });      sendResponse({ success: true });    } else if (message.action === ACTION_APPLY_SCHEDULE) {      const data = await getStorage([        STORAGE_SCHEDULE_ENABLED,        STORAGE_SCHEDULE_START,        STORAGE_SCHEDULE_END,      ]);      await this.applySchedule(data);      sendResponse({ success: true });    } else if (message.action === ACTION_OPEN_XHS) {      const lang = message.lang;      const domain = lang === "zh" ? XHS_DOMAIN_ZH : XHS_DOMAIN_EN;      Filter.openAppWindow(`https://${domain}`);      sendResponse({ success: true });    }  }

Readable equivalent of the constants

assets/js/constants.js
export const DL_API_URL =  "https://api.old-panda.com/image/xiaohongshu/?post_id=";export const ACTION_GET_DL_DATA = "getDlData";

External hosts involved in this download path

    • api.old-panda.com

    Receives the Xiaohongshu post ID and xsec_token in the query string and returns media metadata for the extension's download workflow.

    • www.xiaohongshu.com

    Source page where the extension content script reads the post URL when the download button is clicked.

    • www.rednote.com

    Alternate source page domain accepted by the same content-script download function.

Where it sends data

Destinations our analysis observed Xiaohongshu for Desktop 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.

  • translate.googleapis.comwidely used

    Xiaohongshu for Desktop sends data to translate.googleapis.com. A widely used service: 92 other extensions we have analyzed send data here.

  • api.old-panda.com

    Xiaohongshu for Desktop sends data to api.old-panda.com. 3 other extensions we have analyzed send data here.

Updated 30 September 2026kdbmbkoigckegcfgcfmfpdkbbnhjcooo