An export API documents limit as an integer between 1 and 100. The handler checks only limit > 100. A caller sends -1; the database adapter interprets that sentinel as no limit and returns the full result set. Schema validation passed, the comparison passed, and the response violated the business boundary the field was meant to enforce.
Numeric parameters are compact policy languages. Page size controls volume, timeout controls authority duration, retry count controls external work, quantity controls money or inventory, and concurrency controls fan-out. Type checking confirms representation; it says nothing about units, closed intervals, sentinel values, overflow, conversion, or downstream meaning.
Testing should therefore assert the protected effect. A 200 for a negative number may be safe if the server normalizes it. A finding exists when the request exceeds the declared row, cost, time, money, or work boundary. Trace the value across resolver, service, adapter, queue, billing, and serialization before deciding what it did.
Key Takeaways
- A numeric type is not a numeric policy; every security-relevant number needs an explicit accepted domain, units, and boundary behavior.
- Checks that enforce only
value <= maxoften admit negative values, while checks that enforce onlyvalue >= minmay admit unbounded positive work. - Different layers can assign different semantics to the same value, turning a harmless-looking negative, zero, null, or overflowed number into a limit bypass.
- Pagination and batch controls must be enforced at the data-access or work-admission boundary, not only in a client or GraphQL resolver.
- Strong testing observes rows, objects, tasks, cost, or state changes rather than treating a successful response as proof of impact.
- Durable fixes use shared bounded types or constructors so invalid numeric states cannot travel through the application.
Every Numeric Parameter Has a Contract
Developers often document a field with a type and a short description:
take: Int
Maximum number of records to return.
The actual contract is much larger:
accepted values: integers from 1 through 100
omitted value: server chooses 25
null: rejected or treated exactly like omission
zero: returns zero records, not unlimited records
negative: rejected before data access
too large: rejected or clamped according to documented policy
overflow: rejected during coercion
units: records
scope: per request, after authorization filters
enforcement: query builder and response serializer
If any element is undefined, a downstream component will define it implicitly. That implicit behavior is where bypasses appear.
Type Validity Is Only the First Gate
GraphQL's standard integer type has representation and coercion rules. A valid signed integer can still be invalid for a page size. JSON similarly distinguishes numbers at the syntax level but leaves application range and meaning to code. A language runtime may accept a value that the database driver, ORM, cache, or message broker interprets differently.
Security review should separate three layers:
- Representation domain: Can the transport and runtime represent the value?
- Business domain: Is the value meaningful and permitted for this field?
- Effect domain: What work or access does the downstream system perform with it?
Passing layer one says nothing about layers two and three.
How Negative Values Escape Maximum Checks
A common implementation looks reasonable:
if requested > configured_max:
reject
query.limit(requested)
For a maximum of one hundred, -1 > 100 is false. The value proceeds. What happens next depends on the adapter:
- It may interpret
-1as no limit. - It may omit the limiting clause.
- It may count from the end.
- It may cast to an unsigned value and become enormous.
- It may throw an exception after expensive work begins.
- It may normalize to zero.
- It may use a framework-specific sentinel.
The vulnerability is not the negative number alone. It is the composition of an incomplete policy check with a downstream semantic that increases authority or work.
Maximum Is Not a Complete Predicate
For a bounded count, the invariant is normally:
minimum <= requested <= maximum
But even that may be incomplete. The field may also require:
- Integer, not floating point.
- Finite value.
- Exact unit.
- No implicit string coercion.
- No scientific notation.
- No negative zero ambiguity.
- No overflow during conversion or arithmetic.
- No multiplication that exceeds a downstream bound.
- Consistent behavior when omitted or null.
The safest design validates once into a domain-specific type such as PageSize, RetryBudget, or PositiveQuantity, then prevents raw numbers from reaching deeper layers.
The Dangerous Values Are More Than Negative One
Testing only -1 creates a shallow regression. Numeric-domain testing should cover semantic classes.
Negative Values
Try boundary-adjacent and extreme negatives: -1, minimum minus one, a large negative, and the minimum representable integer. Different code paths may use negation, absolute value, or unsigned casts that behave differently at the minimum integer.
Zero
Zero is often ambiguous. It can mean none, default, unlimited, disabled, or immediate. A batch size of zero might correctly process nothing, while a timeout of zero might disable timeout enforcement. Document field-specific behavior.
Maximum Plus One
This is the conventional upper-bound test and should remain. Also test the largest transport-representable value and a value that overflows after multiplication, such as count * item_size or page * page_size.
Fractions and Scientific Notation
The API layer may reject them, round them, truncate them, or coerce them. Alternative transports can differ. A REST endpoint receiving JSON and a GraphQL resolver calling the same service should produce the same domain decision.
Strings and Booleans
Loose coercion can turn "0", "-1", empty string, true, or whitespace into numbers. Some languages treat failed conversion as zero, which can activate a sentinel meaning.
Null and Omission
They are not automatically equivalent. Omission may apply a safe default while explicit null bypasses a constructor, or the reverse. Follow both paths to the effective query or operation.
Duplicate and Conflicting Values
HTTP query parameters can repeat. Gateways, frameworks, and application code may select first, last, or all values. A validator may inspect limit=10 while the data layer consumes limit=-1 from a different representation.
Numeric Limits Protect More Than Availability
The obvious impact of an unlimited query is resource consumption, but numeric-policy failures can cross several security properties.
| Field | Intended policy | Unsafe alternate meaning | Potential impact |
|---|---|---|---|
| Page size | Bounded records | Negative means unlimited | Data overexposure, memory pressure |
| Offset | Non-negative position | Negative rewinds or errors late | Unexpected records, expensive scans |
| Batch count | Bounded operations | Zero/negative disables cap | Fan-out, queue saturation |
| Quantity | Positive purchasable amount | Negative credits inventory or balance | Financial abuse |
| Price/discount | Bounded rate | Sign inversion | Underpayment or payout |
| Retry count | Small non-negative count | Negative means infinite/default | External-call amplification |
| Timeout | Positive duration | Zero disables timeout | Resource retention |
| Concurrency | Positive worker cap | Negative/zero selects automatic maximum | Cost and availability |
| Retention days | Approved range | Negative skips deletion or deletes immediately | Compliance or data loss |
| Token lifetime | Positive bounded duration | Overflow or sign changes expiry | Authentication persistence |
| Coordinate/range | Valid ordered interval | End before start underflows length | Large allocation or policy bypass |
| Version number | Existing positive version | Negative selects latest/sentinel | Authorization on unintended object |
This is why business logic testing must observe the protected effect. A negative page size that safely returns an empty list is not equivalent to a negative quantity that increases account credit.
Cross-Layer Semantics Create the Bypass
Numeric data often changes shape several times:
JSON or GraphQL value
-> framework scalar
-> resolver argument
-> service DTO
-> language integer
-> ORM option
-> database dialect
-> result serializer
Each conversion can reinterpret the value. A resolver may check the original integer, while a helper substitutes a default for values evaluated as false. An ORM may use a negative sentinel. A database driver may reject it. A serializer may apply a second cap after all rows were loaded, hiding the expensive query while protecting only response size.
Clamping Can Hide Inconsistency
Clamping values into a permitted range is sometimes appropriate, particularly for public read APIs. But silent clamping creates ambiguity and can differ across layers. If the resolver clamps to one hundred but a background export path calls the service directly, the service may remain unbounded.
When clamping is used:
- Perform it in one authoritative domain function.
- Record the effective value.
- Apply the same rule across transports and workers.
- Ensure cost is bounded before clamping requires expensive parsing.
- Return documentation-consistent behavior.
For security-sensitive quantities, explicit rejection is often easier to reason about and audit.
Put Enforcement Near the Protected Effect
Client-side controls improve usability but do not enforce policy. Resolver checks are useful but can be bypassed by another adapter. The deepest reusable service or repository layer should require a validated bounded type.
An effective pattern is:
raw input
-> strict transport coercion
-> domain constructor(min, max, units)
-> authorization and tenant filter
-> bounded query/work admission
-> bounded serialization
The data-access layer should never interpret arbitrary caller numbers as sentinels. Sentinel values can exist internally, but they should be represented by explicit types or options that untrusted input cannot construct.
Authorization Filters Must Come First
For list operations, apply tenant and object authorization before pagination. Fetching an unlimited global set and filtering in application memory is both a data-exposure risk and a resource risk. The database query should include authorization predicates and a validated limit in the same execution plan.
Code-Aware Autonomous Testing Workflow
Numeric-domain testing becomes useful when it is guided by code and effect.
1. Discover Numeric Inputs
Extract arguments from schemas, route definitions, request types, forms, configuration, queue messages, and internal APIs. Prioritize names such as limit, take, first, last, offset, count, quantity, amount, price, timeout, ttl, retry, workers, depth, version, and days.
2. Infer the Intended Domain
Use documentation, UI constraints, constants, database schemas, tests, and business rules. Do not assume every count starts at one; zero can be legitimate. Record uncertainty instead of inventing a policy.
3. Trace the Value to Its Effect
Follow validation, conversion, defaulting, arithmetic, query-building, scheduling, and serialization. Identify every alternate entry point that calls the same service without the primary resolver.
4. Create Synthetic Oracles
Use controlled data with known counts, a fake downstream provider, a bounded queue, or a disposable ledger. The oracle should answer how many records, jobs, retries, dollars, or state transitions actually occurred.
5. Exercise Equivalence Classes
Test valid boundaries and invalid semantic classes, not every integer. Include direct variables, inline literals, alternate transports, duplicate parameters, stored values, and worker messages where applicable.
6. Replay After Remediation
Confirm early rejection and confirm valid behavior at the minimum and maximum. A fix that rejects every value or silently changes default pagination is not complete.
Practical Numeric-Domain Test Matrix
| Input class | Example | Expected policy | Effect to verify |
|---|---|---|---|
| Omitted | No field | Documented default | Default bounded work |
| Null | Explicit null | Reject or same as omission | No alternate unbounded path |
| Minimum | 1 for positive count |
Allow | Exactly one unit |
| Zero | 0 |
Field-specific behavior | None/default is explicit |
| Negative adjacent | -1 |
Reject | No query sentinel reached |
| Large negative | Far below minimum | Reject | No unsigned cast or overflow |
| Maximum | Configured cap | Allow | Work equals cap |
| Maximum plus one | Cap + 1 | Reject or documented clamp | Work never exceeds cap |
| Largest integer | Transport maximum | Reject | No overflow in adapter |
| Minimum integer | Transport minimum | Reject | No negation overflow |
| Fraction | 1.5 |
Reject for integer field | No truncation |
| Scientific notation | Equivalent large value | Consistent coercion | No parser discrepancy |
| Numeric string | "10" |
Strict documented behavior | No loose conversion |
| Empty string | "" |
Reject | No conversion to zero sentinel |
| Boolean | true |
Reject | No conversion to one |
| Duplicate values | Safe then unsafe | Reject ambiguity | Validator/consumer use same value |
| Product overflow | page * size exceeds range |
Reject | No wrapped offset |
| Alternate API | Same service via REST/worker | Same domain policy | Adapter parity |
| Concurrent requests | Valid near-cap values | Aggregate quota | Tenant remains bounded |
The matrix should be specialized per field. A timeout and a quantity do not share the same correct zero behavior.
Evidence of Downstream Resource Impact
A report should show:
- Field, endpoint, operation, and authenticated role.
- Intended minimum, maximum, default, and units.
- Where that policy is declared.
- Exact input class and effective normalized value.
- Resolver and downstream adapter behavior.
- Query, task, ledger, inventory, or state effect.
- Number of returned and internally fetched records.
- Tenant and authorization filters applied.
- Resource, financial, confidentiality, or integrity impact.
- Required data volume and deployment settings.
- Valid boundary control and fixed-build replay.
Avoid claiming "unlimited" from a 200 response alone. Count the result and inspect downstream work. A backend may safely clamp while the response omits that detail. Conversely, a response may return one hundred records after the service fetched a million and sliced them in memory. Both output and internal cost matter.
Severity is contextual. Exposing all records within the caller's tenant may be availability or excessive-data risk. Bypassing authorization filtering can become cross-tenant disclosure. Negative quantity can create financial integrity impact. Infinite retries can affect a paid provider or shared queue. Describe the proven effect rather than assigning one consequence to every numeric bug.
Remediation Patterns
- Define minimum, maximum, default, units, and zero/null behavior for every security-relevant number.
- Parse with checked conversion and reject overflow or unsupported coercion.
- Construct a domain-specific bounded type once.
- Remove public access to internal sentinel values.
- Enforce the bound in the shared service or data-access layer.
- Apply authorization predicates before limit and offset.
- Use checked arithmetic for offsets, ranges, prices, and durations.
- Bound response serialization independently from query execution.
- Add per-principal and per-tenant aggregate quotas where repeated near-cap requests matter.
- Return deterministic errors without leaking hidden maximums when that information is sensitive.
- Retain cross-adapter numeric-domain regression tests.
Schema directives can express some constraints, but do not rely on them as the only control. Internal callers, background workers, migrations, and alternate APIs may bypass transport validation. Domain invariants belong in reusable application code.
CI/CD and Observability
Trigger focused tests when schema arguments, DTOs, coercion helpers, ORM versions, query builders, pagination libraries, billing arithmetic, queue settings, or service adapters change. A diff-aware test can identify affected numeric fields and run their equivalence classes without a full application scan.
Useful telemetry includes:
- Rejected values by semantic class, without logging secrets.
- Effective page or batch size after normalization.
- Rows scanned versus rows returned.
- Queue tasks admitted per request and tenant.
- Retry and timeout values at execution.
- Arithmetic overflow and conversion errors.
- Requests at the configured maximum.
- Negative and zero values by endpoint.
- Adapter disagreement between API and worker paths.
- Resource cost per returned item.
Do not automatically treat every negative input as an attack. Some domains legitimately use signed numbers. Alert when a value violates the field's declared domain or produces an effect beyond policy.
Release Acceptance Criteria
A numeric-policy fix is ready to ship only when the allowed and denied domains are both observable at the effect boundary. The release evidence should name the field, policy version, minimum, maximum, units, default, and behavior for zero, null, and omission. It should then show that the minimum and maximum still produce the intended work while every adjacent invalid class is rejected before query construction, job admission, billing, or state mutation.
Run the same cases through each reachable adapter. A resolver-level fix is incomplete when a REST route, scheduled worker, import job, or internal RPC can still pass a raw number to the shared service. Compare normalized values and effect counts across those paths. If clamping is intentional, record both the requested and effective value and prove that no expensive operation happened before normalization.
Risk acceptance should not say only that "the endpoint is authenticated." Record which roles can reach it, the maximum effect one request can cause, aggregate tenant controls, monitoring, owner, and expiry. An authenticated low-privilege user may still exploit a query or cost amplification path repeatedly. The final gate should fail when a test obtains more records, tasks, retries, time, money, or authority than the domain contract allows, even if the response status and schema remain valid.
Numeric Domain Review Questions
- Is the numeric field's domain documented beyond its type?
- Are minimum, maximum, default, units, zero, null, and omission defined?
- Can negative values pass a one-sided maximum check?
- Can zero disable a limit, timeout, retry, or safety feature?
- Can values overflow during conversion, multiplication, addition, or negation?
- Do duplicate parameters produce ambiguous selection?
- Do GraphQL, REST, workers, and internal calls share enforcement?
- Does any downstream adapter assign sentinel meaning to raw input?
- Are authorization filters applied before pagination?
- Are rows fetched, tasks created, and costs observed, not only response length?
- Are aggregate tenant limits present for repeated near-cap calls?
- Does CI test valid boundaries as well as invalid classes?
Frequently Asked Questions
Does a GraphQL Int automatically reject negative values?
No. A signed integer can be valid at the GraphQL type level. Whether a negative value is permitted depends on the argument's application-specific domain. A page size usually needs an explicit non-negative or positive constraint.
Is clamping a value to the maximum safer than rejecting it?
It can be, if the behavior is deliberate, centralized, documented, and consistent across every caller. For financial or security-sensitive fields, rejection is often clearer. Silent clamping must not hide expensive work performed before the clamp.
Why test zero separately from negative values?
Zero frequently acts as a sentinel for default, unlimited, disabled, immediate, or empty behavior. Its correct meaning differs by field, so it deserves an explicit invariant and regression case.
Can a response cap protect an unbounded database query?
Not necessarily. The service may fetch or scan a large result and then serialize only a small subset. Enforce the limit in the query or work-admission layer and observe internal rows or cost.
What should an autonomous test use as its oracle?
Use the protected effect: returned records, rows scanned, jobs created, provider calls, account balance, inventory, token lifetime, or state transitions. Status codes alone cannot distinguish a safe normalization from a real bypass.
The Business Limit Lives in the Observable Effect
Generic integers become easier to govern when the application gives them domain meaning: a unit, minimum, maximum, default, and clearly defined exceptional values. Validation before conversion catches one class of error, while a second boundary near row selection, retry creation, duration, quantity, or fan-out protects the effect from downstream sentinel and overflow behavior.
We prefer tests organized around semantic classes and observed outcomes rather than a catalogue of unusual numbers. The lasting assertion is not that -1 always returns one status code. It is that no representable input can make the operation exceed the business or resource limit the field is supposed to express.
#APISecurity #GraphQLSecurity #BusinessLogic #InputValidation #SecureCoding #FuzzTesting
Find issues like this in your own codebase
SecureHup runs this exact exploit chain — discovery, enumeration and cross-tenant validation — on every deployment, automatically.