Skip to content

Fix Parameter #50/#52 authz on repo approval and email association (includes ME_DATA escape) - #444

Merged
martinmain93 merged 4 commits into
masterfrom
claude/fire-authz-50-52-9d23be
Oct 21, 2026
Merged

martinmain93 merged 4 commits into
masterfrom
claude/fire-authz-50-52-9d23be

Conversation

@martinmain93

@martinmain93 martinmain93 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Fixes two open Parameter pentest highs on fire.fundersclub.com. The branch is cut from security/xss-escape-and-hsts, so it includes d4f4e41 (ME_DATA JSON escape + HSTS, also open as #442). Merging this PR ships all three; #442 can be closed afterwards or merged first.

⚠️ Deploy promptly. Deploys are manual (Heroku Procfile), and prod likely still lacks the d4f4e41 escape. Today #52 plants an attacker-controlled sender email into a repo admin's ME_DATA, so the whole chain is probably exploitable end to end in prod right now.

#52: EmailMap write with no object authorization

POST /api/github/associate-email/<msg_uuid>/ only checked is_authenticated, so any logged-in user could create an EmailMap on any repo.

  • The offer-associate email now links to /associate-email/<msg_uuid>/<token>/. The token is django.core.signing, salt associate-email, payload {msg uuid, from_email}, and it expires after 30 days (fb_github/utils.py).
  • AssociateEmailView GET and POST need a valid token for that exact message and sender, or the caller must be an admin of the repo (Repository.is_editable_by). A bare msg_uuid now returns 404 on GET and 403 on POST.
  • A duplicate (repo, email) returns 409 instead of a 500 IntegrityError.
  • Frontend: new associate-email/:uuid/:token route. The token stays in the path, so it survives the OAuth login redirect. The token-less route is kept for admins.

#50: Repo approval gated by a self-chosen username

POST /api/github/repository/<uuid>/approve/:

  • Removed the dispatch override that set AllowAny, so IsAuthenticated applies.
  • approve() only approves when the caller's linked GitHub SocialAccount.uid equals original_invitation_data['inviter']['id'], and only while the status is pending-inviter-approval. It no longer compares the inviter login with the Django username.
  • Frontend: the logged-out "Verify your GitHub" page (authenticate/:uuid) called this endpoint anonymously to show the repo name and inviter login. It now shows generic copy. Logged-in flows are unchanged.

Also: close local password signup

Added ACCOUNT_ADAPTER.is_open_for_signup → False, so outsiders can no longer make a local account with an arbitrary username. That is what made both findings reachable. SOCIALACCOUNT_ADAPTER.is_open_for_signup → True keeps GitHub signup open: by default allauth's social adapter defers to the account adapter, which would otherwise close GitHub signup too. Existing local accounts can still log in. It's in its own commit (08bce73) so it can be reverted separately.

Behaviour changes to be aware of

  • "Link your GitHub" emails sent before this deploy have token-less links, so they now only work for repo admins. Senders get a new link the next time they're offered one.
  • Not changed: on association, EmailMap.login is still request.user.username, and approve_by_inviter also uses the username. With signup closed, new accounts come from GitHub, but any existing local accounts keep their self-chosen usernames.

Overlap with #443

Tests

  • test_approve rewritten: anonymous calls get 403, and the repo is only approved by the inviter's GitHub uid. New negative cases cover a local user whose username equals the inviter login, and a different linked GitHub account.
  • New AssociateEmailAPITestCase: a raw uuid is rejected, and so are bad, foreign, wrong-salt, wrong-email and expired tokens. Covers the sender with a token, the repo admin without one, an admin of another repo (rejected), and the duplicate (409).
  • test_association_email checks the emailed link has a valid token.
  • New test_signup: local signup is closed, GitHub signup is open.

python manage.py test: 20 tests, OK (flake8 included). Run locally on Python 3.10, with psycopg2 2.8.6 swapped in locally and a collections.abc shim for the old celery. The lockfile is unchanged. The Angular changes were not compiled locally (no node_modules). They're small, and I kept them TS 2.0 / AOT-safe.

🤖 Generated with Claude Code

martinmain93 and others added 4 commits June 19, 2026 10:36
Fixes a stored XSS: app_html injects MeView.get_me_data() into a <script>
block via json.dumps(), which does not escape <, >, or &. A repo's emailmap
contains attacker-influenced sender email addresses, so a crafted from_email
could break out of the script element and execute in a repo admin's browser.
Escape <, >, & to their \uXXXX forms before injection (U+2028/U+2029 are
already escaped by json.dumps' ensure_ascii default), matching Django's
json_script scheme.

Also add HSTS response headers to production (SECURE_HSTS_SECONDS=1y,
includeSubDomains, preload), which were previously unset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parameter #52: POST /api/github/associate-email/<msg_uuid>/ only
checked is_authenticated, so any logged-in user could create an
EmailMap on any repo. The offer-associate email now links to
/associate-email/<msg_uuid>/<token>/, where the token is a signed,
expiring (30 day) django.core.signing payload of the message uuid and
sender address. GET and POST require that token, or that the caller is
an admin of the repo. A duplicate (repo, email) now returns 409 instead
of a 500 IntegrityError.

Parameter #50: POST /api/github/repository/<uuid>/approve/ was
AllowAny and approved the repo for any user whose (self-chosen) Django
username matched the inviter's GitHub login. The AllowAny override is
gone, and approval now requires the caller's linked GitHub
SocialAccount uid to equal original_invitation_data['inviter']['id'],
still only while the repo is pending inviter approval.

The anonymous "Verify your GitHub" page used the approve endpoint to
show the repo name and inviter login; it now shows generic copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Accounts are only meant to come from GitHub login. Open local signup let
anyone create an account with an arbitrary username, which is what made
#50 and #52 reachable for outsiders. The social adapter stays open so
GitHub signup keeps working (by default it defers to the account
adapter).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uv rejects the git:// URL for the pinned django-allauth commit, which
fails the "Requirements lockfile" check on master and this branch.
Same change as #443: use git+https for the same commit, record that
source in the lock, add a setuptools build constraint for its legacy
setup.py, and have lock checks keep the existing pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@martinmain93
martinmain93 merged commit aaef60a into master Oct 21, 2026
2 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.

1 participant