base / base/base-verify-demo

Sybil protection bypass via delete then re claim + frontend/backend status code mismatch

Open
#24 3 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
199
Forks
50
PR merge metrics
No merged PRs in 30d

Description

---

Summary

Two bugs found in the sybil protection and verification flow:

Bug 1 (Critical): Delete-then-re-claim bypasses anti-sybil protection

The delete-airdrop endpoint removes a user's record from the database, but does not track the verification token afterwards. This allows a user to claim the airdrop multiple times with different wallets using the same verified social account.

Steps to reproduce:
1. Connect Wallet A → verify X account → get token abc123 → claim airdrop ✅
2. Call POST /api/delete-airdrop with Wallet A's signature → record deleted ✅
3. Connect Wallet B → verify the same X account → get the same token abc123
4. POST /api/verify-token → findUnique({ where: { baseVerifyToken: "abc123" } }) returns null (old record was deleted)
5. Upsert creates a new record with Wallet B's address and token abc123 ✅
6. User claims the airdrop again with a different wallet 🎉

Affected file: pages/api/verify-token.ts (lines 83-103) — the token uniqueness check only looks at existing records, not deleted ones.
Affected file: pages/api/delete-airdrop.ts (whole file) — deletes without tracking the token.

Impact: The core anti-sybil mechanism described in the README ("Same provider account always produces same token → Database rejects duplicate tokens → prevents multi-wallet abuse") is completely bypassable. A user can claim the airdrop N times with N different wallets using the same social account.

Suggested fix: Add a DeletedToken table (or a deleted_tokens set) that tracks tokens from deleted claims. On re-verification, check both the existing records AND the deleted-token list before allowing a new claim.

---

Bug 2 (Minor): Frontend checks for HTTP 400, backend returns HTTP 412

In pages/index.tsx, the frontend checks for response.status === 400 to show a "traits not satisfied" error message:

typescript
// pages/index.tsx (frontend)
if (response.status === 400 && errorData.message === 'verification_traits_not_satisfied') {
setVerificationError('Sorry, your X account does not have a blue checkmark...')
}

However, in pages/api/verify-token.ts, when the Base Verify API returns a 400 with verification_traits_not_satisfied, the backend maps it to HTTP 412 (Precondition Failed):

typescript
// pages/api/verify-token.ts (backend)
if (verifyResponse.status === 400) {
const errorData = JSON.parse(responseBody);
if (errorData.message === 'verification_traits_not_satisfied') {
return res.status(412).json({ // <-- 412, not 400
error: 'X account does not satisfy verification requirements.'
});
}
}

Impact: The error handling branch for "traits not satisfied" is dead code on the frontend. Users who don't meet trait requirements see a generic error message instead of the helpful "no blue checkmark" message.

Suggested fix: Change the frontend check from response.status === 400 to response.status === 412, or change the backend to return 400 instead of 412.

---

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reading pages/api/verify-token.ts lines 83-103 and pages/api/delete-airdrop.ts to trace token handling across deletion and re-verification. Then inspect pages/index.tsx and the backend response mapping for the status mismatch. Done means deleted verification tokens cannot be reused for another claim, and trait failures reach the frontend branch that shows the specific guidance.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, full-stack, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.