Heatrail

Chapter 09

Why the network cannot be enumerated

The first party with a real reason to attack a shared fraud signal is not a fraudster. It is a member: a shop with a valid key, an invoice paid on time and a question about somebody else's customers. The key already grants the ability to send requests, so the question is one request away. This article is about why that request does not answer it.

The property

Every check is anchored to a real order or session reference, so the API cannot be used as a lookup tool. Answers are categories about the order in front of the shop, never records. The one bulk exception is a single history scan per Pro shop, at most 250,000 rows, set out below. A request for anything belonging to another shop is answered as if it did not exist.

Why it holds

The first attacker holds a valid key

Threat modelling for a shared network tends to start at the perimeter, with somebody outside trying to get in. That is not where the interesting attack is. Everybody who can reach this API holds a credential we issued to a company we approved, and one of the things such a member might want is a genuine attack: walk a space of identifiers, read what comes back, and reconstruct the fraud history of every other shop in the network. We name it as the realistic first attack in our own threat model on the security page, because a shared signal that answers questions about identifiers is one that can be sifted.

The answer to it is not detection. The first four answers are the shape of the interface itself, because a property of the request format holds whether or not anybody is watching.

A check is about something that happened

A check must carry a real order reference or a real session reference. At least one has to be present, and a request with neither never becomes a check at all: it fails validation, before anything is looked up, and nothing about it is written down. That single requirement changes what a request means. It converts "please look this up" into "this happened at my checkout, and you can be asked about it".

An order reference is not a decoration on the payload. It points into the sender's own order table, and the sender can be asked to produce the order, so a probe stops being a free question and becomes an assertion about the prober's own business. A stream of references nobody can produce an order for is not a query log. It is a statement about the shop that sent it.

Categories, not records

What comes back describes the order in front of the shop. It is a score, a set of reasons written to be read by whoever is handling that order, and an opaque alias for whatever the check matched. What it never contains is another shop's evidence: not an identifier, not a hash, not somebody else's order reference, not the name of the shop that applied a label.

This is what makes volume useless rather than merely expensive. Sifting works when every grain carries a little of what you are after, so that enough grains accumulate into a picture. Here each grain carries a verdict about the asker's own order and no fragment of anybody else's, so a thousand of them compose into a thousand verdicts about the asker's own orders.

Figure 1

Two requests at one door. Without an order reference the request is refused. With one it receives a category rather than a record.On the left, two request boxes. The upper box is a check carrying neither an order reference nor a session reference. A solid arrow takes it to a tall box in the middle labelled one door, POST slash v1 slash check, and a dashed arrow brings it straight back. A note under the upper box reads 422 invalid payload, refused, and nothing is stored. The lower box is a check carrying an order reference for a checkout that happened. A solid arrow takes it into the same door and a solid arrow carries an answer out to the right. On the right stand two cards. The first is drawn solid, marked returned, headed Category, and lists a score, reasons about this order, and a trail alias that is yours. The second is drawn dashed, marked never returned, and headed Record with the word struck through, and it lists an order from another shop, identifiers, and evidence rows. A line at the foot reads that through this door a survey costs one real checkout per answer, and that the one history scan per shop is capped at 250,000 rows.Checkorder_ref: absentsession_ref: absent422 invalid_payloadrefused, and nothing is storedCheckorder_ref: ord-10241a checkout that happenedOne doorPOST /v1/checkone anchoredquestion at a timeno list, no rangeno exportreturnedCategorya scorereasons aboutthis ordera trail aliasthat is yoursnever returnedRecordan order fromanother shopidentifiersevidence rowsHere a survey costs one real checkout per answer. The one history scan is capped at 250,000 rows.
A check without an order is refused, and a check with one gets an answer about that order and nothing else.

One capped exception, and no stable handle

The check takes no list, no range and no filter, and there is no export. That is the design: an interface answering one anchored question at a time makes the cost of a survey linear in checkouts the surveyor must be able to explain, and nothing there scales in the attacker's favour.

One interface is exempt from the anchor rule, and its bounds are fixed in code. A Pro shop may run one history scan in its life: a single session of at most 250,000 past orders, each row answered strong, weak or none in the same kind of categories, with no row kept. Those rows are not anchored to a checkout, so for that one session a hard limit takes the place of the cost argument above: one session per shop, 250,000 rows, and a second scan only after an operator reset recorded with its reason in the audit log. The limits are on the security page.

The second half is the alias. When a check matches something, the response carries an opaque handle for it, and that handle is per shop: the same trail appears under a different identifier to every shop that sees it. So two members comparing notes cannot tell whether they hold one trail or two, and an alias cannot be accumulated across shops into a stable key. It is good for the one thing a shop needs it for, which is recognising that two of its own orders touched the same history.

404 rather than 403

Ask for another shop's order and the answer is 404 not_found, exactly the answer for an order that never existed anywhere. The obvious status code is 403, and choosing against it is the control.

403 means this exists and you may not have it. That is one bit, and one bit is the whole of enumeration: walk a space of references, keep the ones that answer 403, and you have learned which references are real elsewhere in the network. 404 returns the same nothing for "no such order" and for "not your order", so the walk ends with as much information as it started with, and the same uniformity holds on the internal interface the partner portal uses. The cost is a less helpful error for an integrator who typed the wrong reference, and that trade is deliberate.

Behind those four sits one behavioural measure, and its existence is the whole of what is published about it: a key whose traffic stops looking like checkout traffic is frozen for review.

What it costs to evade

What a probing member must spend

Every probe is a signed request under your own key, issued to a company that signed a contract, so there is no anonymous position to ask from. Every probe carries an order reference, which is a claim that a checkout happened in your shop and one you can be asked to produce the order for. A survey worth the name is a durable written record, under your own name, of a great many checkouts that did not happen.

What they gain

One category answer about one order they already had. Not another shop's identifiers, not its labels, not its evidence, not its name, and nothing that composes with the next answer into a picture of somebody else's business. And probing at a volume that would tell you anything is the same thing as making your key stop working, so the two ends never meet: the cheap version learns nothing, and the version that might learn something does not stay available. The one history scan is the stated exception: once in a shop's life, it answers up to 250,000 rows without a checkout behind any of them.

Verify it

Both halves are checkable with a key of your own, and neither check needs anything belonging to anybody else. The first is the anchor: send an otherwise valid check with neither reference on it.

# Signed for real, and with an Idempotency-Key: both are checked ahead of the
# payload, so a placeholder signature answers 401 and a missing key answers 400
# before the anchor rule ever gets a chance to speak.
curl -s -X POST https://api.heatrail.eu/v1/check \
  -H "Content-Type: application/json" \
  -H "Authorization: HR1 {key id}:{signature}" \
  -H "Idempotency-Key: chk-verify-anchor-rule-01" \
  -d '{"ip":"203.0.113.42"}'

# 422 invalid_payload. The details name order_ref and session_ref: at
# least one must be present. Nothing was looked up, nothing was stored.

The second is the uniform answer. Ask for an order reference belonging to another shop, then ask again for one you invented on the spot that belongs to nobody at all.

curl -s https://api.heatrail.eu/v1/orders/ord-not-yours/explain \
  -H "Authorization: HR1 {key id}:{signature}"

curl -s https://api.heatrail.eu/v1/orders/ord-nobody-has-this/explain \
  -H "Authorization: HR1 {key id}:{signature}"

# 404 not_found, twice. Same status, same body. The reference that exists
# somewhere else and the reference that exists nowhere are one case.

The reasoning behind that status code is set out at isolation on the security page, and the field names and error codes above are the ones in the API reference.

  • Identity without identities, on the other half of the argument: an identifier that never arrives in the clear is of little use to somebody who cannot ask us about it.
  • Trails, on what the alias in a response points at, and why it is a property of history rather than a record about a person.
  • Reasons you can read, on why a reason can be readable without being evidence.
  • Isolation, on the security page, for the 404 decision in full.

Heatrail

This site is not public yet

Heatrail is in a private pre-launch period. Enter the access password to continue.

Enquiries: hello@heatrail.eu