Skip to content

Security: XNinety9/c411trend

Security

SECURITY.md

Security

C411 Trend runs unsandboxed inside omarchy-shell, like every Omarchy plugin. It reads your C411 statistics with a personal API key you create on c411.org with the read-only "Profile (read)" scope (profile:read). This page says exactly what it touches, where the key goes, and which limits keep a misbehaving server or a corrupt file from hurting the shell.

Since 2.0 the plugin reads no browser data: no cookie stores, no keyring, no decryption. 1.x did; that code is gone.

What it never does

  • No privilege escalation: nothing runs as root, no sudo/pkexec, no policy files, no services, no install scripts. It is QML, JavaScript and one Python script (bin/c411trend-fetch) running as your user.
  • No network access except one HTTPS request to https://c411.org/api/user/profile per poll (every 30 minutes by default; C411 allows 30 requests a minute per key). No redirects are followed, no other host is contacted, no telemetry.
  • The key is never written to shell.json, never put on a command line (ps cannot see it), never logged or printed, and never sent anywhere but c411.org.
  • No writes outside its own files: ~/.config/c411trend/api-key (the key), ~/.local/state/c411trend/ (history and its lock), and its entry in ~/.config/omarchy/shell.json through the shell's settings API.

The API key

Step How
Pasting it The panel's field is masked (echoMode: Password) and cleared as soon as you press Enter. The key goes to c411trend-fetch --store-key on stdin; the QML drops it from memory once written.
Storing it Validated against a strict token pattern ([A-Za-z0-9._~+/=:-]{16,512}), then written atomically to ~/.config/c411trend/api-key, mode 0600, in a 0700 directory. A symlink at that path is replaced, never written through.
Reading it Opened with O_NOFOLLOW, at most 4 KiB, re-validated; a group- or world-readable file is tightened to 0600. A key that fails the pattern is refused before any request.
Sending it As Authorization: Bearer … in a curl config read from stdin (-K -). The pattern excludes quotes, backslashes, spaces and newlines, so the key cannot alter that config.
Scope Only profile:read is needed. Do not tick other scopes: the plugin uses none, and a key that can only read your profile is the least you can lose. Revoke it on c411.org at any time.

What it stores

~/.local/state/c411trend/history.jsonl (directory 0700, file 0600): one line per poll with a timestamp, uploaded/downloaded byte counts, ratio, upload and download credit, XP, level and Zen. No key, no username, no torrent data. Deleting the directory resets the plugin.

The latest full profile (username, team, class, alerts) is kept in memory only. The widget's entry in shell.json holds your settings and the last what-if question (simQuery, at most 64 characters).

Trust boundaries

Source Trusted? Handling
Your settings yes Intervals, targets, language, templates. --lang accepts en or fr only.
The pasted key no (validated) Strict pattern before storing and before every use; see above.
c411.org's answer no Capped at 1 MiB while streaming and cut off 5 s after the request timeout. The HTTP status is read separately (401 → key refused, 403 → scope missing, 429 → rate limited, 5xx → server error). The body must be a JSON object; byte counts, XP and Zen must be finite, non-negative numbers below 10^21 or they are dropped (the sample is rejected without transfer totals). Booleans must be real booleans. Every string (username, team, class, block reason, freeleech sources, bonus labels) is stripped of control characters and cut (24 to 200 characters); lists are cut to 5 or 8 items; unknown shapes are dropped, not guessed at. Fields the panel does not use (id, avatar URL) are not passed on.
history.jsonl no (it can be hand-edited) Lines that are not objects with finite, non-negative numbers are skipped. Files over 16 MiB are read from the tail.
Bar template (barFormat) yes, but bounded Single-pass substitution: a value containing {…} is never expanded again. Only the widget's own keys match ({constructor} or {__proto__} stay literal text). The resulting label is cut at 160 characters.
What-if field and when IPC yes, but bounded At most 64 characters, parsed by one anchored pattern without nested repetition. Never evaluated or passed to a process.

All text the widget shows, the bar label included, uses Text.PlainText: a username, team name or block reason cannot inject markup. Messages come from fixed English and French catalogues, filled with values, never built from them.

How the request is made

curl -q -sS --compressed --proto =https --max-redirs 0 \
     --max-filesize 1048576 -m 15 -A "C411Trend (+omarchy plugin)" \
     -H "Accept: application/json" -w "\n%{http_code}" \
     -K - https://c411.org/api/user/profile
  • -q comes first so ~/.curlrc is ignored: a trace or verbose line there would otherwise print the key.
  • The Authorization header arrives on stdin as a curl config line (-K -).
  • Only the last line of curl's stderr is ever shown, stripped of control characters and cut to 200 characters.

Limits

What Limit
Response body 1 MiB, read while streaming; curl is killed past it or 5 s after the request timeout
One request 15 s (--timeout), never more than the whole-run --deadline (60 s)
Shell-side watchdog 90 s, then the process is stopped
Polls every refreshIntervalMin (30 min default, 5 min minimum), and when the panel opens if the last poll is over 5 minutes old: far under C411's 30 a minute
API key file 4 KiB
Bar label 160 characters
What-if question 64 characters
History file compacted past 2 MiB: every sample of the last 90 days, manual samples, one per 6 h before that; rewritten atomically under an flock

Verifying

  • npm test: model tests (node) and script tests (Python) run against a stub of the documented endpoint. They cover the key on stdin and never in argv, ~/.curlrc isolation, key validation and storage (permissions, symlinks, injection attempts), every HTTP status the API documents, oversize and slow answers, invalid numbers and booleans, string sanitising, history permissions and compaction, bar-template and what-if bounds, and that both message catalogues carry the same placeholders.
  • npm run check: the above, then the Omarchy plugin marketplace's Automated Security Baseline on the committed tree, which must report passed with no findings and no capabilities, then omarchy plugin validate. CI runs the baseline against a pinned marketplace commit.

Screenshot tooling

docs/demo/ is for maintainers and is never run by the plugin. To take screenshots without real data, photo-session.sh temporarily swaps your history and bin/c411trend-fetch for a made-up history and a stub that never contacts c411.org. It restores both, and shell.json, byte for byte on exit, error or Ctrl-C, and checks the result. Only a SIGKILL can stop that; the script prints where the originals are kept before it changes anything. It never touches your API key.

Reporting

Open an issue, or for anything sensitive contact the author through GitHub (XNinety9) before publishing details.

There aren't any published security advisories