What a RedNote Share Link Can Reveal
I came across a website on X that checks whether a shared link reveals the sharer’s social account. I was curious: when I send someone a post, what else goes with it?
I started by looking at the request the site sent when I clicked its query button. Calling that endpoint from Python would have been easy enough, but it wouldn’t explain how the lookup worked. I wanted to try the calculation myself, using one of my own links.
RedNote turned out to be a manageable example. I put together a small Python script for the token format I’d found; it needs only the standard library and works offline.
Table of Contents
The parameter to look for
A RedNote share URL can contain several IDs. The one in the /explore/ path belongs to the post; I was looking for the account behind the share.
The parameter I needed was shareRedId. I found the decoding method in cnlnn’s 分享链接是如何暴露分享者的. That article describes a reversible transformation of a 24-character hexadecimal account ID:
- Convert the ID to uppercase.
- Add a fixed offset to each character’s ASCII value, using a different offset for each position.
- Base64URL-encode the resulting 24 bytes.
To decode it, reverse those steps. There is no secret key involved: the fixed offsets are enough.
The method and the sample token below come from that article, under CC BY-NC-SA 4.0. I added input checks and a command-line interface.
Make the decoder fussy
I wanted malformed input to fail clearly, especially if RedNote changed the format later.
The script accepts a long RedNote URL, a Markdown link containing it, or a raw token. It normalises HTML-escaped ampersands and the escaped ampersands that can appear when copying Markdown. For URL input, it checks the hostname and requires exactly one non-empty shareRedId parameter.
Then it checks the token itself:
- exactly 32 Base64URL characters;
- decoded bytes which become exactly 24 uppercase hexadecimal characters after subtracting the offsets;
- an exact match when those characters are encoded again.
The script returns the account ID in lowercase and builds a profile URL from it. Anything outside the supported format raises an error.
For a short link, you’ll need to follow its redirect first and copy the resulting long URL. The script doesn’t do that network request for you.
The Python script
Here is the complete script, including the calculation, input handling and command-line entry point. It uses only Python’s standard library. Copy this block into a local file named rednote_share_id.py to try it.
#!/usr/bin/env python3
"""Offline decoder for the known 24-byte RedNote shareRedId format.
Algorithm and synthetic vector: cnlnn, CC BY-NC-SA 4.0.
https://creativecommons.org/licenses/by-nc-sa/4.0/
Reference:
https://cnlnn.pages.dev/posts/share-link-identity-tracking/
No HTTP requests, third-party dependencies, or account enumeration.
"""
import argparse
import base64
import html
import json
import re
import sys
from urllib.parse import parse_qs, urlsplit
OFFSETS = (2, 6, 2, 0, 3, 5, 4, 9, 6, 7, 5, 2,
9, 8, 0, 6, 6, 3, 9, 7, 4, 5, 6, 9)
def extract(value):
"""Decode one token or long share URL; reject unknown formats."""
value = html.unescape(value.strip().replace(r"\&", "&"))
urls = re.findall(r'https?://[^\s<>"\)]+', value)
if urls:
if len(urls) != 1:
raise ValueError("Supply exactly one share URL.")
parsed = urlsplit(urls[0])
host = (parsed.hostname or "").lower()
if host != "xiaohongshu.com" and not host.endswith(".xiaohongshu.com"):
raise ValueError("Supply a RedNote long URL, not a short link or another domain.")
params = parse_qs(parsed.query, keep_blank_values=True)
candidates = params.get("shareRedId", [])
if len(candidates) != 1 or not candidates[0]:
raise ValueError("Expected one nonempty shareRedId parameter.")
token = candidates[0]
else:
token = value
if not re.fullmatch(r"[A-Za-z0-9_-]{32}", token):
raise ValueError("Unsupported shareRedId: expected 32 Base64URL characters.")
raw = base64.b64decode(token, altchars=b"-_", validate=True)
recovered = bytes((byte - shift) % 256 for byte, shift in zip(raw, OFFSETS))
if not re.fullmatch(rb"[0-9A-F]{24}", recovered):
raise ValueError("Token does not match the known UID encoding.")
uid = recovered.decode("ascii").lower()
encoded = base64.urlsafe_b64encode(
bytes(byte + shift for byte, shift in zip(recovered, OFFSETS))
).decode("ascii")
if encoded != token:
raise ValueError("Token failed the exact round-trip check.")
return {
"platform": "RedNote",
"account_id": uid,
"profile_url": "https://www.xiaohongshu.com/user/profile/" + uid,
"evidence_parameter": "shareRedId",
"round_trip_matches": True,
"profile_verified": False,
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("value", nargs="?", help="Share URL, Markdown link, or raw shareRedId")
args = parser.parse_args()
try:
value = args.value
if value is None:
value = input("Paste share URL or shareRedId: ") if sys.stdin.isatty() else sys.stdin.read()
print(json.dumps(extract(value), ensure_ascii=False, indent=2))
return 0
except (ValueError, EOFError) as exc:
print("Error: " + str(exc), file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())
There is one offset per decoded byte. The 32-character token gives us 24 bytes, so zip processes all of them after the length check. Subtraction wraps modulo 256; the hexadecimal check then rejects values that don’t fit the expected encoding.
keep_blank_values=True keeps empty query parameters so I can reject them. Duplicate parameters also fail: there’s no good reason to guess which one was intended. The leading dot in the hostname suffix check prevents a lookalike domain from passing as a subdomain.
Re-encoding the recovered bytes should reproduce the token exactly. This checks the calculation, not the identity of the sender.
After saving the code above as rednote_share_id.py, run it with a raw token or a quoted long URL. With no argument it prompts for input; redirected standard input works too.
python3 rednote_share_id.py 'Mjc0Mzc6OkA-QEZETExFTDY0Ozo4OjxA'
Trying a sample token
Here is cnlnn’s artificial test vector, so you can try it without using a personal link:
| Stage | Value |
|---|---|
Input shareRedId | Mjc0Mzc6OkA-QEZETExFTDY0Ozo4OjxA |
| Base64URL-decoded bytes, shown as text | 27437::@>@FDLLEL64;:8:<@ |
| Recovered account ID | 0123456789abcdef01234567 |
| Re-encoded token | Mjc0Mzc6OkA-QEZETExFTDY0Ozo4OjxA |
The odd-looking text in the second row is what comes straight out of Base64URL decoding. Subtract the offsets to get the ID; add them back and encode again to recover the input token.
Checking my own link
The output has two Boolean fields that are easy to confuse:
round_trip_matches: truemeans the decoded ID encodes back to the original token. It doesn’t tell us whether the account exists.profile_verified: falseis always false, because the script never loads the profile.
My own token passed the round-trip check. I opened the resulting URL in a browser and, sure enough, it was my profile. A plain HTTP fetch hadn’t returned the profile details, so I needed the browser for that check. The internal account ID was also different from the RedNote account number shown on my profile.
A forwarded link can still carry the original sharer’s token, though. If a mate sends me somebody else’s link, decoding it may lead to that person’s account rather than my mate’s.
A few regression checks
Save this beside rednote_share_id.py to check the expected result and a few inputs that should fail:
from rednote_share_id import extract
TOKEN = "Mjc0Mzc6OkA-QEZETExFTDY0Ozo4OjxA"
result = extract(TOKEN)
assert result["account_id"] == "0123456789abcdef01234567"
assert result["round_trip_matches"] is True
assert result["profile_verified"] is False
# Derive the real platform origin from the decoder's profile URL.
origin = result["profile_url"].split("/user/profile/")[0]
for value in (
"", "abc", "A" * 32, TOKEN + "=",
origin + "/explore/example",
origin + "/?shareRedId=" + TOKEN + "&shareRedId=" + TOKEN,
):
try:
extract(value)
except ValueError:
pass
else:
raise AssertionError("Unexpectedly accepted: " + repr(value))
My full tests also cover copied Markdown, HTML-escaped query strings, short links and lookalike hostnames. The artificial token is enough to test the calculation, so I’ve kept my real share URL and account details out of the fixtures.
What the link gave away
I meant to share a post, but the URL also provided a route to my public profile. It didn’t expose a password or grant access to private posts. Still, that was more than I’d intended to send.
Removing the extra parameters is a separate job. Some may affect whether the post opens, so I’d want to check the cleaned link before relying on it. For this experiment, understanding how the account ID got there was enough.
