sh1pt browser: Chrome Web Store and AMO appeal recipes - #1014
Merged
Conversation
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
vu1nz Security Review0 finding(s) in PR #? No security issues found. |
ThreatCrush Security Scan49 finding(s) HIGH/CRITICAL: 1 | MEDIUM: 14 | LOW: 34
Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two recipes for store chores that have no API.
chrome-web-store— listing fields and unpublishThe 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:
Ten conditions, none of them settable by the API.
Unpublishing is dashboard-only too, established by trying rather than by reading docs:
POST /items/{id}/unpublishPOST /items/{id}/publish?publishTarget=unpublishedPOST .../publish?deployPercentage=0DELETE /items/{id}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.askrather 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
googleprofile withgoogle-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 pointingmeta-appatgoogle.amo-appeal— appealing a Mozilla decisionA 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) andtemplates/abuse/appeal.html(#appeal-submit,#appeal-thank-you).Two facts from that source shape the code:
clean_emailcompares 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 whysubmitAppealnever retries.statusneeds 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 returnsis_disabled_by_mozillain the body, so a 401 body is informative. Checked against addon3061765and slugcoinpay-wallet(both disabled),marksyncr(public), and an unknown slug (404, correctly not reported as disabled).Testing
61 tests green across the package,
tsc --noEmitclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01WEo9454bWwK8L8HAxKFCk2