[00.00_r1gor]
[00.00_bugbounty]
r1gor

all write-ups

Full Financials of Every Listing on a Business Marketplace, From a One Minute Old Account

----[ Target

The target is a brokerage that operates a marketplace for the private sale of profitable online businesses. Listings are worth between five thousand and several million dollars. The brokerage charges sellers to list and gates the most sensitive data on each listing behind a buyer side verification flow. The published policy commits the platform to protect two pieces of information per listing until the buyer completes identity and proof of funds verification: the live URL of the business, and the full profit and loss history. One of the two is properly gated. The other is not.

----[ Setup

A new account was created for this test. Email verified, nothing else. The account has the following state at the moment of the request:

ID Verified:           none
Liquidity Verified:    none
Remaining Unlocks:     0
Unlock Max Listing:    none
Role:                  customer

Zero verifications. Zero unlocks. The user is not allowed to view any private listing data under the published policy.

----[ Observation

The single page application calls one endpoint to render a listing page:

GET /api/v1/listings/show?listing_number=NNNNN
Authorization: Bearer <session>

The response includes the gated URL field as a redacted string, which is correct. It also includes a field named metrics. The metrics field is a list of monthly objects. Each object contains revenue, expenses, profit, advertising spend ratio, and cost of goods ratio. The values are real. The data is delivered to a user who has unlocked nothing.

{
  "listing_number": NNNNN,
  "asking_price": NNNNNNN,
  "country_of_registration": "XX",
  "metrics": [
    { "month": "YYYY-MM",
      "revenue":  REVENUE,
      "expenses": EXPENSES,
      "profit":   PROFIT,
      "tacos_pct": NN.N,
      "cogs_pct":  NN.N },
    { "month": "YYYY-MM", ... },
    ...
  ]
}

The bundle ships the reason. The render path branches on a flag that comes from the user record, not from the response:

const isPrivate = user.unlocked.includes(listing_number);
const placeholder = [...Array(23)].map(() => ({
  grossRevenue: Math.random() * 99e3 + 1e3,
  netProfit:    Math.random() * 99e3 + 1e3
}));

(isPrivate ? response.metrics : placeholder).map(...)

When the user has not paid, the chart is drawn from a local array of random numbers. The real numbers are still in the response, already on the wire, already in the browser, just not selected for rendering.

----[ Scale

The listing identifier is a sequential integer. A single loop walks the full active inventory:

for n in $(seq START END); do
  curl -s -H "Authorization: Bearer $T" \
    "https://[API]/api/v1/listings/show?listing_number=$n"
done

A few hundred lines of script reproduce the entire competitive intelligence database that the brokerage sells access to. Sample rows from a live run, on the same unverified account:

[redacted #1]  affiliate, business niche      asking ~5.5M    margin ~50%
[redacted #2]  ecommerce, supplements         asking ~1.5M    margin ~20%
[redacted #3]  service, automotive            asking ~1.4M    margin ~35%
[redacted #4]  amazon fba, apparel            asking ~430k    margin ~15%
[redacted #5]  amazon fba, beauty             asking ~400k    margin ~20%

Months of profit and loss history are returned for every row.

----[ Why It Matters

The brokerage charges buyers for unlocks and requires identity and proof of funds before private data is shown. The gate is a business model, not a feature. Removing it harms three groups at the same time.

o  Sellers, who agreed to confidential listing terms, find the
   monthly profit history of their business available to any
   email address on the internet.

o  Competing brokerages and acquirers receive a free feed of
   the entire asking price and margin distribution of the
   inventory.

o  The brokerage loses the regulatory cover it relies on to
   require buyer verification at all.

The bug also weakens the surrounding controls. Verification is the gate, the gate is unenforced, so the verification flow exists mostly as a deterrent against the honest user.

----[ Remediation

The authorization check belongs on the server. The metrics field must be filtered out of the response when the requesting user has not unlocked the listing, regardless of what the bundle does with the value. The placeholder logic on the client should remain as a visual fallback, not as a privacy boundary.

The unlock state already exists in the user record consulted by the same endpoint. The fix is one conditional in the serializer.

----[ Closing

The listing page works. The verification flow works. The unlock flow works. The defect is a single field that the server returns to every caller and the browser is asked to hide. The browser is not where access control lives.

Reported.

A control that runs in the client is a suggestion to the client.