Skip to content

sh1pt browser: Chrome Web Store and AMO appeal recipes - #1014

Merged
ralyodio merged 2 commits into
masterfrom
worktree-cws-recipe
Sep 6, 2026
Merged

sh1pt browser: Chrome Web Store and AMO appeal recipes#1014
ralyodio merged 2 commits into
masterfrom
worktree-cws-recipe

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Two recipes for store chores that have no API.

chrome-web-store — listing fields and unpublish

The Web Store Publish API creates an item, puts a package on it, and publishes it. Everything it checks before publishing is dashboard-only. A real refusal, taken verbatim from the API:

Publish condition not met: ... mandatory privacy information ...; A justification for remote code use is required.; A justification for host permission use is required.; ... certify that your data usage complies ...; Language is not selected.; Please select a Category ...; Icon image is missing.; At least one screenshot or video is required.; The detailed description is too short ...; You have published the maximum allowed number of 3 extensions.

Ten conditions, none of them settable by the API.

Unpublishing is dashboard-only too, established by trying rather than by reading docs:

Attempt Result
POST /items/{id}/unpublish 404
POST /items/{id}/publish?publishTarget=unpublished 400 Invalid Value
POST .../publish?deployPercentage=0 400, ineligible for partial rollouts
DELETE /items/{id} 404

That matters because the cap counts published items, so a fourth extension cannot ship until one comes down.

Reviewer note — the selectors here are guesses. The Google account this package signs in with has a stale password, so no run has reached the console. The DOM interactions park through session.ask rather than throw, and the Developer Program Policies attestation is deliberately left to a human rather than clicked from a guessed selector. The pure functions are the parts to trust.

This also required narrowing the registry's profile-uniqueness assertion, since the recipe shares the google profile with google-cloud-oauth. That assertion's stated reason — "a shared profile would sign one registry out when the other signs in" — is a fact about distinct identity providers, not about recipes; the Cloud console and the Web Store console are one Google account, so a separate profile forces a redundant second sign-in. It now forbids two providers sharing a profile, and was verified to still fail (naming the collision) by temporarily pointing meta-app at google.

amo-appeal — appealing a Mozilla decision

A Mozilla-disabled add-on 403s every write, listing-only PATCHes included, so no privacy policy can be attached and no version uploaded. Reads still work. No API lifts the block; only an appeal a human decides.

These selectors are not guesses — addons-server is open source, so they come from abuse/urls.py (appeal/<decision_cinder_id>/), abuse/forms.py (AbuseAppealForm.reason, the conditional email field) and templates/abuse/appeal.html (#appeal-submit, #appeal-thank-you).

Two facts from that source shape the code: clean_email compares the email against the address the decision was sent to, so it is not free-form; and appeals are throttled 20/day per IP and per user, which is why submitAppeal never retries.

status needs no browser and no credentials, and was verified against the live API rather than mocked — AMO answers 401 to an unauthenticated read of a disabled add-on but still returns is_disabled_by_mozilla in the body, so a 401 body is informative. Checked against addon 3061765 and slug coinpay-wallet (both disabled), marksyncr (public), and an unknown slug (404, correctly not reported as disabled).

Testing

61 tests green across the package, tsc --noEmit clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WEo9454bWwK8L8HAxKFCk2

The Web Store Publish API creates an item, puts a package on it and publishes
it. Everything it *checks* before publishing is dashboard-only. A real refusal
lists ten unmet conditions — privacy information, remote-code and host-permission
justifications, the policy certification, language, category, icon, screenshot,
description length, and the 3-extension publisher cap — and the API can satisfy
none of them.

Unpublishing is dashboard-only too, established by trying rather than by reading
docs: POST /items/{id}/unpublish 404s, publishTarget=unpublished is an Invalid
Value, deployPercentage=0 is refused as an ineligible partial rollout, and DELETE
404s. That matters because the cap counts published items, so a fourth extension
cannot ship until one comes down.

The recipe shares the `google` profile with google-cloud-oauth, which required
narrowing the registry's profile-uniqueness assertion. Its stated reason — "a
shared profile would sign one registry out when the other signs in" — is a fact
about distinct identity providers, not about recipes; the Cloud console and the
Web Store console are one Google account, so a separate profile would force a
redundant second sign-in. The assertion now forbids two *providers* sharing a
profile, and was verified to still fail (naming the collision) by temporarily
pointing meta-app at `google`.

Tested: id validation, URL building, the empty-title unpublished check, listing
validation against Google's 132/25 character limits, splitting the real API
refusal into its ten conditions, and slot ranking that offers the least-used
listing first and never offers an unpublished one.

NOT tested, and flagged in the file: every selector. The Google account this
package signs in with has a stale password, so no run has reached the console.
The DOM interactions are first guesses that park through session.ask rather than
throw, and the compliance attestation is deliberately left to a human instead of
being clicked from a guessed selector.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEo9454bWwK8L8HAxKFCk2
A Mozilla-disabled add-on 403s every write, not just the disable flag — a PATCH
carrying nothing but listing copy is refused too, so no privacy policy can be
attached and no version uploaded. Reads still work. No API lifts the block; the
only route back is an appeal a human decides.

Unlike the Chrome recipe in the previous commit, these selectors are NOT guesses.
They come from Mozilla's own source, which is open:

  abuse/urls.py              appeal/<str:decision_cinder_id>/
  abuse/forms.py             AbuseAppealForm.reason (Textarea),
                             AbuseAppealEmailForm.email
  templates/abuse/appeal.html  #appeal-submit, #appeal-thank-you

Two facts from that source shape the code. The email field is conditional and
clean_email compares it against the address the decision was sent to, so it is
not free-form. And appeals are throttled 20/day per IP and per user, which is
why submitAppeal never retries — a moderation queue is not somewhere to spray.

`status` needs no browser and no credentials, and is verified against the live
API rather than mocked: AMO answers 401 to an unauthenticated read of a disabled
add-on but still returns is_disabled_by_mozilla in the body, so discarding a 401
as an auth failure throws away the only signal that matters. Checked just now —
addon 3061765 and slug coinpay-wallet both report the Mozilla disable, marksyncr
reads public, and an unknown slug is correctly not reported as disabled.

61 tests green across the package; tsc clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEo9454bWwK8L8HAxKFCk2
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

49 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 14 | LOW: 34

Severity Rule Location
HIGH js-host-header-trust packages/bots/wechat/src/index.ts:405
MEDIUM redos-nested-quantifier packages/actions-fleet-core/src/action-pack/schema.ts:3
MEDIUM redos-nested-quantifier packages/core/src/setup-helpers.ts:583
MEDIUM redos-nested-quantifier packages/policy/src/rules/bundle-id.ts:3
MEDIUM sql-string-concatenation packages/targets/deploy-wordpress/src/index.ts:154
MEDIUM redos-nested-quantifier packages/targets/desktop-linux/src/index.ts:19
MEDIUM redos-nested-quantifier packages/targets/desktop-mac/src/index.ts:15
MEDIUM redos-nested-quantifier packages/targets/desktop-steamos/src/index.ts:28
MEDIUM redos-nested-quantifier packages/targets/mobile-android/src/index.ts:9
MEDIUM redos-nested-quantifier packages/targets/mobile-ios/src/index.ts:11
MEDIUM redos-nested-quantifier packages/targets/tv-androidtv/src/index.ts:14
MEDIUM redos-nested-quantifier packages/targets/tv-firetv/src/index.ts:13
MEDIUM redos-nested-quantifier packages/targets/tv-tvos/src/index.ts:14
MEDIUM redos-nested-quantifier packages/targets/tv-webos/src/index.ts:26
MEDIUM js-unescaped-html-sink sites/sh1pt.com/app/blog/[slug]/page.tsx:76
LOW secret-generic-credential packages/affiliates/skimlinks/src/index.test.ts:25
LOW secret-generic-credential packages/affiliates/skimlinks/src/index.test.ts:71
LOW secret-generic-api-key packages/affiliates/sovrn/src/index.ts:28
LOW secret-generic-credential packages/agent-providers/opencode/src/__tests__/opencode.test.ts:99
LOW js-nosql-injection packages/ai/amazon-bedrock/src/index.test.ts:121
LOW secret-generic-credential packages/ai/amazon-bedrock/src/index.ts:9
LOW secret-generic-credential packages/ai/amazon-bedrock/src/index.ts:10
LOW secret-generic-credential packages/ai/amazon-bedrock/src/index.ts:11
LOW secret-generic-credential packages/bridges/matrix/src/index.ts:58
LOW secret-generic-credential packages/bridges/matrix/src/index.ts:59
LOW secret-generic-credential packages/bridges/slack/src/index.test.ts:259
LOW secret-generic-credential packages/captcha/captchasolver/src/index.ts:34
LOW secret-generic-credential packages/cli/src/commands/secrets.ts:189
LOW secret-generic-credential packages/cloud/linode/src/index.ts:15
LOW secret-generic-credential packages/observability/sentry/src/index.ts:15
LOW secret-generic-credential packages/outreach/producthunt/src/index.ts:103
LOW secret-generic-credential packages/promo/posthog/src/index.ts:23
LOW secret-generic-credential packages/scanners/threatcrush/test/scan-output.txt:35
LOW secret-generic-credential packages/scanners/threatcrush/test/scan-output.txt:40
LOW secret-database-url packages/scanners/threatcrush/test/scan-output.txt:54
LOW secret-generic-credential packages/security/snyk/src/index.ts:26
LOW secret-generic-credential packages/social/hashnode/src/index.ts:4
LOW secret-generic-credential packages/social/linkedin/src/index.ts:3
LOW secret-generic-credential packages/social/linkedin/src/index.ts:4
LOW secret-generic-credential packages/social/medium/src/index.ts:4
LOW secret-generic-credential packages/social/snapchat/src/index.ts:5
LOW secret-generic-credential packages/social/tiktok/src/index.ts:5
LOW secret-generic-credential packages/targets/plugin-vscode/src/index.test.ts:115
LOW secret-generic-credential packages/targets/registry-ans/src/index.test.ts:79
LOW secret-generic-credential packages/targets/registry-ans/src/index.ts:49
LOW secret-generic-credential packages/targets/sdk-pypi/src/index.test.ts:49
LOW secret-generic-credential packages/vcs/gitlab/src/index.test.ts:96
LOW secret-generic-credential sites/sh1pt.com/supabase/config.toml:303
LOW secret-generic-credential sites/sh1pt.com/supabase/config.toml:335

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 3360e90 into master Sep 6, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants