eXportArticle is a userscript that runs on x.com/twitter.com with elevated userscript
permissions. This document explains what it can access, why, the trust model, and how to
report a vulnerability.
Please report suspected vulnerabilities privately via a
GitHub security advisory rather than a public issue, so a
fix can ship before details are public. Include the script version (@version), browser +
userscript manager, and reproduction steps. We aim to acknowledge within a few days.
The userscript metadata block requests:
| Grant | Why |
|---|---|
GM_xmlhttpRequest |
Fetch media bytes from *.twimg.com to base64-inline them. A normal page fetch can't read these cross-origin bytes; this is the whole reason it's a userscript. |
unsafeWindow |
Read X's own API/GraphQL responses in your browser to discover the full MP4 URL behind a video. No page data is injected back; only response bodies are inspected locally. Also used to reach the File System Access API (showDirectoryPicker) for Save to library. |
GM_registerMenuCommand, GM_unregisterMenuCommand |
Add the Save to library settings (layout / contents / change folder) to the userscript-manager menu. No page or network access. |
@connect twimg.com, cdn.syndication.twimg.com |
Allow the media fetch to reach X's media/syndication hosts. |
@connect x.com, twitter.com |
Resolve canonical post URLs/metadata. |
@run-at document-start |
Install the video-discovery hook before X's own scripts run, so early API responses aren't missed. |
Accesses (all locally, in your browser):
- Media bytes from
*.twimg.com(images, videos, posters, avatars) — the in-code fetch is restricted totwimg.comhosts, in addition to the@connectallowlist. - X's own
fetch/XHR API responses, read only to extract MP4 video variants. This is the same data the page already loaded for you. - The DOM of the article/post you choose to export.
Local storage and file writing (Save to library):
- Writes export files to a folder you explicitly pick via the browser's File System Access
API. The script only writes the export's own files (
post.html/post.llm.md/media/) into that chosen folder and its dated subfolders; it never reads your existing files, and it cannot access any location you didn't grant. On browsers without that API it instead triggers a normal.zipdownload. No new network access — this is local disk only. - IndexedDB stores the chosen folder's handle so you don't re-pick it each time;
localStoragestores two small preferences (layout, contents). Both are on thex.comorigin, local to your browser, and never transmitted.
Never:
- ❌ Sends your data to any third party. There is no server, no analytics, no telemetry.
The only outbound requests are to
twimg.com/syndication to download media for embedding. - ❌ Takes any action on your account (post, like, follow, DM).
- ❌ Reads anything you can't already see on the page (no passwords, no DMs).
The exported .html is opened later from file://, so it is built defensively:
- No dynamic code from page content. The exporter uses no
innerHTML,eval,new Function, ordocument.write; the export is assembled as escaped strings. - Allowlist sanitization. Tweet/article rich text is reduced to a fixed set of safe
inline tags (
a,br,code,strong,em); any other element is dropped to escaped text. A<script>in X's DOM can never be reproduced as a tag. - Link-scheme allowlist (
safeUrl). Every URL that becomes anhrefis restricted tohttp(s)/mailto, sojavascript:/data:/vbscript:links cannot execute when the archive is opened. Covered by a unit test. - The network-capture bridge posts only to
location.origin, and the receiver verifies the message origin, same-window source, and a payload tag before processing.
The companion .llm.md contains no base64, data URLs, CSS, or scripts by design.
The userscript's @updateURL/@downloadURL point at the main branch of this public
repository. Installing it means trusting this repo's maintainers and GitHub's delivery: a
change merged to main will auto-update on installed clients. Mitigations:
mainis the default branch and should be branch-protected; releases are tagged.- To avoid auto-update, install a specific tagged release and disable auto-update in your userscript manager.
The codebase was reviewed for the first public release. No high-severity issues were found;
the privileged paths (DOM→string export, GM_xmlhttpRequest fetch, the unsafeWindow
network-capture bridge) are conservative. Low-severity hardening from that review —
safeUrl link-scheme allowlist, twimg.com fetch host check, and location.origin
message targeting/validation — is implemented in v0.5.0.