notes 2026-08-18 · 6 min read

Business Logic Vulnerabilities: Attacking the Rules

Business logic testing: sequence abuse, negative values, coupon and discount state machines, multi tenant checks and concurrency as a logic bug.

Business Logic Vulnerabilities: Attacking the Rules

A scanner can report perfect headers while the application lets the same coupon run fifty times.

Business logic vulnerabilities appear when the system executes the programmed flow correctly but accepts a state that should never exist.

These bugs are hard to automate because they depend on understanding what the application is trying to protect.

Price.

Balance.

Limit.

State.

Sequence.

Role.

Quantity.

A tool sees parameters.

The pentester needs to see rules.

The examples below assume a lab or an authorized penetration test.

Business rule and unintended behavior

First identify the rule that should exist

Before changing a request, I try to explain the feature in one sentence.

“A coupon can be used once per account.”

“An order can only ship after payment.”

“A normal user cannot choose their own role.”

“A transfer cannot exceed the available balance.”

“A promotion allows at most three units.”

If you cannot state the rule, proving that the application violated it becomes difficult.

This also prevents strange behavior from being reported as a vulnerability without real impact.

The interface is not a security rule

Consider a cart request:

1
2
3
4
POST /cart HTTP/1.1
Content-Type: application/x-www-form-urlencoded

productId=31&quantity=1&price=19900

The browser displays the correct price.

That does not mean the backend should trust price=19900.

Change it:

1
price=100

If the server accepts that value, critical logic has been placed on the client side.

This pattern appears in price, shipping and discount. Commission, currency, limits and roles are common too.

Anything affecting a business decision needs server-side validation.

Sequence is an attack surface

Many workflows assume users will follow the screens.

Login.

MFA.

Role selection.

Confirmation.

Checkout.

Payment.

Completion.

Burp Repeater does not follow screens.

You can send step four before step three.

You can repeat step two.

You can return to step one after step four.

You can call the final endpoint directly.

PortSwigger has several labs where the flaw begins with this exact assumption.

The application assumes that reaching the final endpoint means earlier steps were completed.

Server-side state needs to prove that.

Validate state at every transition

Think about an order:

1
CREATED -> PAID -> SHIPPED -> DELIVERED

Now test:

1
CREATED -> SHIPPED

Or:

1
CANCELLED -> SHIPPED

Or:

1
REFUNDED -> PAID

The UI may never provide those buttons.

The API may still accept the transition.

Incomplete state machines are classic logic-flaw territory.

Breaking the expected sequence

Negative values are still values

Applications often validate a maximum and forget the minimum.

Quantity:

1
2
3
4
{
  "productId": 31,
  "quantity": -10
}

Balance operation:

1
2
3
{
  "amount": -500
}

Percentage:

1
2
3
{
  "discount": -20
}

The result may reverse an operation.

Add money.

Increase inventory.

Reduce a total.

The exact effect depends on implementation.

I test zero, negative and maximum. Then I try maximum plus one, unexpected decimals and large values.

Each input tests a specific assumption.

Per-operation limits can fail in aggregate

Consider a wallet with a daily limit of 10,000.

The backend validates:

1
transfer_amount <= 10000

Two transfers of 9,000 both pass.

The actual rule was:

1
sum(transfers_today) + new_transfer <= 10000

The local check looked correct.

The global rule was missing.

This pattern appears in coupons, withdrawals and invitations. Credits, uploads, token issuance and quota consumption behave similarly.

Discounts are state machines too

Consider two coupons.

WELCOME10.

NEWSLETTER20.

The application blocks the same coupon twice in a row.

But it allows:

1
2
3
4
WELCOME10
NEWSLETTER20
WELCOME10
NEWSLETTER20

Validation checked the previous code.

The rule should have checked the full set of benefits already consumed.

This is a state bug, not an input-syntax bug.

Multi-tenant logic needs more than basic authorization

A user can access Project 42.

The project belongs to Organization A.

The same user also belongs to Organization B.

Now the user tries:

1
2
3
4
{
  "projectId": 42,
  "destinationOrganizationId": "B"
}

The user may be authorized for both objects individually.

The business rule may still prohibit transfers between those organizations.

Object authorization alone does not cover the relationship.

Many business logic flaws live at those intersections.

User roles need server-side treatment

Registration:

1
2
3
4
{
  "email": "gabriel@example.test",
  "role": "user"
}

Changing role to admin is the obvious test.

The interesting cases often appear later.

The backend may ignore the field during registration but accept it during profile update.

Or a complete-profile endpoint may reuse the same DTO as an administrative feature.

Different workflows touching the same object may enforce different rules.

Compare endpoints.

A trusted user may later change trusted data

An application validates company affiliation during registration.

Later it allows email changes without repeating the rule.

The user starts with:

1
gabriel@gmail.com

The normal checks pass.

Then the email changes to:

1
gabriel@internal-company.example

If privileges depend only on the current domain, the system may grant access that the original registration flow would have blocked.

Any property used for authorization needs protection when it changes.

Concurrency is business logic too

Two requests can observe the same state before either updates the database.

Balance: 100.

Request A checks whether 80 can be withdrawn.

Request B checks whether 80 can be withdrawn.

Both see 100.

Both pass.

The result may be a total withdrawal of 160.

Race conditions deserve their own article, but they belong here because execution order breaks a business rule.

I test hypotheses, not payload collections

For each workflow, I write down what the system assumes.

Then I try to violate one assumption at a time.

Will users always use the UI?

Will steps always arrive in order?

Will values always be positive?

Will objects always stay inside one tenant?

Will a coupon only be used once?

Will approval always happen first?

Can two requests ever arrive together?

That process produces findings scanners rarely discover.

Business logic testing method

The report should explain the broken rule

“Business logic flaw” is a weak description.

Better:

1
The application allows an order to be confirmed without a recorded payment.

Or:

1
The daily limit is validated per transaction, allowing multiple operations to exceed the configured limit.

Or:

1
The backend allows benefit reuse when different coupon codes are alternated.

The team fixing the bug needs to understand the rule.

Remediation

Document valid states.

Validate transitions on the server.

Derive critical values from trusted sources.

Authorize relationships between objects as well as individual objects.

Enforce invariants in the database where possible.

Use transactions and locking where concurrency affects the rule.

Test paths outside the normal sequence.

Business logic security starts when the team writes down what must never happen and turns those statements into executable validation.

References

PortSwigger Web Security Academy, Business logic vulnerabilities.

PortSwigger Web Security Academy, Examples of business logic vulnerabilities.

This post is licensed under CC BY 4.0 by the author.