Application security guide

Input handling beyond injection, including business logic abuse

Validation cannot stop the attacks that cost the most. A request to refund an order can have a well-formed identifier, a positive amount within limits and a valid session, and still be the second refund on the same order. Nothing in the payload is malformed. The entire class of business logic abuse looks like correct usage, which is why input validation, however thorough, is the wrong control for it and why teams that invest only there stay exposed.

Why does validation not stop logic abuse?

Because validation asks whether each field is well formed, and abuse is a property of the sequence and the state, not of the field. A quantity of one is valid. A quantity of one submitted forty times in parallel against a stock level of one is a different question, and it is not answerable by looking at the request. The same applies to skipping a step, repeating a step, or performing steps in an order the interface never offered.

This is also why input validation is a poor defence against injection, even though it is often described as one. Filtering dangerous-looking characters is a guessing game against an interpreter you do not control, and it breaks legitimate input. The correct control is structural: keep code and data separate at the point where they meet, so that whatever the input contains, the interpreter never treats it as instruction.

Keep validation, but for the reason it is actually good: rejecting input early narrows the state space, makes fuzzing more effective and turns some vulnerabilities into errors. Just do not report it as coverage against the categories it does not cover. A schema that constrains every field, with no invariant enforced anywhere, is a well-validated vulnerable system.

What is the correct model for injection?

Separate the query from the values, at every interpreter boundary, using the mechanism that boundary provides. Parameterised statements for SQL, so the database receives structure and data through different channels. Argument arrays rather than a shell string for operating system commands, so no character can introduce a second command. Context-aware escaping in templates, applied by the template engine, because the correct escaping differs between an HTML body, an attribute, a URL and inline script.

The failures cluster in the places where no parameterisation exists. Sort columns and table names cannot be parameterised, so they need an allowlist mapping caller input to known identifiers rather than escaping. Dynamic filters assembled from a query language need the same treatment. Server-side template rendering of user-supplied templates has no safe escaping story at all, and the answer is to not do it, because template injection commonly leads directly to code execution.

For output, know which sink you are writing into. The same string is safe in a text node, dangerous in an attribute without quotes, and dangerous again inside a script context or a URL scheme. Framework auto-escaping handles most of this and the findings concentrate on the explicit escape hatches, which is why searching for the raw-HTML helper in your front-end framework is one of the highest-yield greps available.

Which input assumptions break in practice?

The ones nobody writes down. Each row below is an assumption that holds in normal use, fails under a hostile caller, and is invisible in a code review unless you are specifically looking for it.

AssumptionWhat an attacker sendsConsequenceControl
This field is a stringAn object or array in the JSON bodyComparison operators reach the query layerValidate the type, not only the presence
The client sends the correct priceA modified amount or currencyPurchase at an arbitrary valueLook up price server side from the identifier
Quantities are positiveA negative or very large numberCredit created, or an overflow in a totalRange checks plus a database constraint
The proxy and the app parse this path the same wayEncoded separators, duplicate headers, odd whitespaceA route gate is bypassed upstream of the checkNormalise once, at one layer, and reject ambiguity
This URL will be fetched safelyAn internal address or a redirect to oneServer-side request forgery reaching internal servicesAllowlist hosts, resolve then check, block redirects
The uploaded file is what its name saysAn archive with paths escaping the target directoryFiles written outside the intended locationReject absolute and relative traversal on extraction
Two identical requests are harmlessThe same request replayed concurrentlyDouble redemption, double refund, double dispatchIdempotency keys and a unique constraint

What does business logic abuse look like?

Ordinary traffic with an unusual shape. A discount code applied through an endpoint the checkout page calls once, called three times. A subscription downgraded and upgraded repeatedly to accumulate pro-rata credits. A returns process started before dispatch and completed after. A quantity field that triggers a bulk pricing tier and is then reduced before payment. Each request is authorised, well formed and rejected by nothing.

The pattern that unifies them is a state machine that exists in the designer's head and in the user interface, but not in the server. If the only thing preventing step four before step three is that the button is hidden, then the constraint does not exist, because an attacker calls the endpoint directly. This is the single most common root cause in this category and it is worth checking on your own highest-value flow today.

Enumeration belongs here too, because it uses the API exactly as intended. An endpoint that returns whether a reference exists, at any rate, is a mechanism for building a list of valid references, and one that returns a small amount of personal data per call is a mechanism for extracting a customer database slowly. The abuse is in the aggregate, which means the control has to be in the aggregate as well.

How do you enforce invariants rather than validate fields?

Write down the statements that must never be false, then put each one somewhere the application cannot bypass. A unique index on order identifier plus refund reference makes a double refund impossible rather than unlikely. A check constraint keeps a balance non-negative even if two code paths disagree. A conditional update that only succeeds when the row is still in the expected state turns a race into a failed write, which is the outcome you want.

Do the arithmetic and the authority server side. Prices, totals, discounts, credits, entitlements and limits should be derived from stored data using identifiers supplied by the caller, never from values supplied by the caller. This one rule removes a large fraction of the category, and the test for it is to intercept a request, change a numeric field, and see whether the outcome changes.

Then add idempotency where repetition is dangerous. A caller-supplied key, stored with the result and returned on repeat, makes retries safe and replays inert. This is normal engineering practice for payment integrations and it is underused everywhere else, despite being the cleanest available answer to concurrent duplicate submission.

How do you detect abuse you did not anticipate?

Log business events, not just requests, and alert on the ones that should be impossible. A refund exceeding the order value, a credit created without a matching debit, a subscription changed five times in an hour, a single account viewing three thousand records in a day. These queries are cheap, they run against data you already have, and they catch the specific thing that design review missed, which is by definition the thing you did not think of.

Reconciliation is the same idea with a longer period. A nightly job that recomputes totals from the event history and compares them against stored balances will find both bugs and abuse, and it is the only control in this guide that detects a successful attack you failed to prevent. Financial systems have done this for a very long time; application teams often skip it and then have no way to establish what happened.

Rate limiting completes the set, applied per account and per resource rather than only per address, because the interesting abuse arrives authenticated. Set the limit from observed legitimate usage, and treat the first breach as an alert rather than a block while you learn the real distribution, so the control ships instead of being deferred over fear of blocking a real customer.

Common questions

What is business logic abuse?
Using an application's own features in a sequence or at a volume the designer never intended, with every individual request being well formed and authorised. Examples include applying a discount code multiple times, starting a return before dispatch, repeatedly cycling a subscription to accumulate credits, and replaying an identical request concurrently. Because nothing is malformed, input validation and security scanners do not detect it.
Does input validation prevent injection?
Not reliably. Filtering dangerous characters is a guessing game against an interpreter you do not control, and it rejects legitimate input. The structural control is keeping code and data separate at each interpreter boundary: parameterised statements for databases, argument arrays instead of shell strings for commands, and context-aware escaping applied by the template engine for output. Validation is still worth doing, for narrowing state rather than as the defence.
What is type confusion in an API?
Sending a value of a different type than the handler expects, most often an object or array where a string was assumed. In systems that pass request values into query builders, this lets comparison operators reach the data layer and change the meaning of a lookup. The fix is validating the type explicitly rather than only checking that a field is present and non-empty.
Why do proxy and application parsing differences matter?
Because a security decision made at one layer can be bypassed at another. If an ingress or gateway normalises an encoded path separator differently from the application behind it, a rule that blocks a route can be satisfied while the application routes the request somewhere else. Normalise once, at a single layer, and reject ambiguous input rather than attempting to interpret it consistently in two places.
How do you stop double-spend and duplicate submission?
Put the constraint in the data store rather than the handler. A unique index across the relevant identifiers makes a duplicate impossible, and a conditional update that only succeeds when the row is still in the expected state turns a race into a failed write. Add caller-supplied idempotency keys stored with the result, so retries are safe and replays return the original outcome instead of creating a second one.
How do you detect logic abuse you did not predict?
Log business events and alert on states that should be impossible: a refund exceeding an order value, a credit with no matching debit, an account reading thousands of records in a day. Add a reconciliation job that recomputes balances from event history and compares them with stored values. That is the only control that finds a successful attack you failed to prevent, and it uses data you already hold.

More on Application security

Let’s create something out of this world together.

Have a project in mind? Contact us for expert design and development solutions. Let’s discuss how we can help grow your business.

Azaadi Offer

Claim a free security assessment

Until 31 August we're covering the cost of a full vulnerability assessment and penetration test. Mention it in your message and we'll scope it with you.

  • Web application testing, authenticated and unauthenticated
  • Mobile application testing across iOS and Android
  • External network and infrastructure assessment
  • Manual exploitation by engineers, not scanner output

Testing and the report are free. Fixing what we find is quoted separately, with no obligation to accept.

Read the full offer

Tell us what you are trying to build and we will tell you plainly whether we are the right people for it. Book a call with an expert to work through the detail, or ask for a fixed quote if the scope is already clear. No obligation either way.

Four fields is all we need to get started.

Fastnexa Logo

© 2026 fastnexa. All rights reserved.