A URL such as ?tag=a&tag=b contains two values for the same query key. Depending on how each layer parses it, an application might see a, b, or both values. That difference can cause confusing bugs—and, when services disagree about security-sensitive fields, parameter-pollution vulnerabilities.
This guide shows the common handling patterns, explains why systems can disagree, and gives practical ways to handle repeated keys deliberately.
Key Takeaways
- A query string can contain the same key more than once; it does not inherently mean “first,” “last,” or “list.”
- Parsers and application code may apply different policies. Confirm the behavior of every relevant layer instead of assuming it is universal.
- For fields that should have one value, reject duplicates. For fields designed to accept multiple values, parse and validate them as a list.
- Apply the same policy at system boundaries, including any proxy, gateway, application, or downstream service that interprets the query.
The same URL can represent different values
Consider this request:
/search?tag=coffee&tag=tea
It has two tag entries. Common handling policies include:
| Policy | Value the application uses |
|---|---|
| First value | coffee |
| Last value | tea |
| List of values | coffee and tea |
| Reject duplicates | An error or validation response |
None of the first three policies should be assumed for every server, framework, proxy, or application. Even within one application, a parser might return a list while later code selects one item. Configuration and the particular API being used matter.
In JavaScript, the standard URLSearchParams interface makes the distinction visible: get() returns the first matching value, while getAll() returns all matching values. The following example also shows how to select the last value explicitly:
const params = new URLSearchParams("tag=coffee&tag=tea");
const first = params.get("tag");
const all = params.getAll("tag");
const last = all.length > 0 ? all[all.length - 1] : null;
console.log(first); // "coffee"
console.log(last); // "tea"
console.log(all); // ["coffee", "tea"]
This demonstrates the behavior of this API, not a universal rule for server frameworks. When using a framework, consult its documentation for the specific request-parsing API and configuration in your application.
Why services disagree
A URL may pass through multiple components before it reaches the code that uses its query values. A proxy or gateway, a web server, a framework parser, and application code can each interpret or transform the request. If one component keeps the first value and another keeps the last, they can make different decisions about what the request means.
That mismatch is especially important when one component checks a value and another acts on it. For example, a request containing role=user&role=admin could be treated as user by an authorization check and admin by downstream logic if the two layers use different policies. This is a general failure pattern, not a claim that a particular framework is vulnerable by default.
Other sensitive cases include permission flags, account identifiers, redirect destinations, payment amounts, and parameters used in request signatures. Duplicate values can also cause non-security bugs, such as filters showing unexpected results or analytics recording inconsistent campaign data.
Choose a policy for each parameter
Decide whether each query key is a scalar, a list, or invalid when repeated. Do not let an accidental parser default make that decision for you.
Scalar values: reject duplicates
If a parameter is intended to contain exactly one value, check the number of values before using it. Rejecting duplicates avoids silently choosing a value that another layer might interpret differently.
function readExactlyOne(params, key) {
const values = params.getAll(key);
if (values.length !== 1) {
throw new Error(`Expected exactly one value for ${key}`);
}
return values[0];
}
const params = new URLSearchParams("account=42");
const account = readExactlyOne(params, "account");
A missing value and an explicitly empty value are different cases: account and account= do not necessarily mean the same thing to your application. Decide whether empty strings are allowed and validate them separately. Also validate the resulting value's type, format, and allowed range; counting values alone is not sufficient validation.
List values: preserve and validate all entries
If repetition is intentional—for example, multiple tags—read the full list and apply limits and validation to every item.
function readTags(params) {
const tags = params.getAll("tag");
if (tags.length > 10) {
throw new Error("Too many tags");
}
if (tags.some((tag) => tag.length === 0)) {
throw new Error("Tags must not be empty");
}
return tags;
}
const params = new URLSearchParams("tag=coffee&tag=tea");
console.log(readTags(params)); // ["coffee", "tea"]
The exact limits and allowed values should follow the application’s requirements. Do not silently switch between list and scalar behavior in different code paths.
First or last value: make the choice explicit
Keeping the first or last value can be appropriate when it is a deliberate, documented compatibility rule. But it is risky for sensitive fields if upstream and downstream components might choose differently. If you must use one of these policies, apply it consistently and ensure validation and business logic use the same parsed representation.
A practical debugging workflow
When a repeated query key produces an unexpected result:
- Capture the exact URL or request. Check whether the key appears more than once, including values added by redirects, clients, or integrations.
- Inspect each parsing boundary. Determine what the edge proxy, server, framework, and application code receive. Check framework documentation and configuration for the specific parsing API in use.
- Log carefully. In a controlled test, record the parsed key count and values. Avoid logging secrets, tokens, personal data, or full sensitive query strings.
- Reproduce the request with duplicates. Test first-value, last-value, list, missing-value, and empty-value cases where relevant.
- Settle on one contract. Reject repeats for scalar fields or explicitly support a validated list. Make downstream components follow the same contract.
- Test the full path. A unit test of application parsing cannot reveal a mismatch introduced by a gateway or another service.
Common Mistakes
- Assuming a repeated key always becomes a list. Some APIs expose all values, while application code or another component may select one.
- Assuming first or last is universally correct. The appropriate policy depends on the parameter’s meaning and must be consistent across the request path.
- Validating only the value eventually used. Check for duplicates before a parser or helper discards information about them.
- Treating a URL parser as a security control. Parsing reveals structure; it does not establish that a value is authorized, safe, or valid for a particular operation.
- Testing only ordinary requests. Include duplicated, absent, empty, and malformed values in tests for sensitive parameters.
Best Practices
- Document whether every query parameter accepts one value, multiple values, or no repeated values.
- Reject duplicate values for security-sensitive scalar parameters unless a well-defined compatibility requirement says otherwise.
- Validate inputs at system boundaries and ensure gateways and downstream services do not interpret the same parameter differently.
- Add integration tests for repeated keys across the actual request path, not only isolated application code.
- When changing parsing behavior, review clients and integrations that may already send repeated keys.
Conclusion
Duplicate query parameters are valid inputs to many URL-processing APIs, but their meaning depends on the parser and application policy. Make that policy explicit: reject repeats for scalar fields, validate every item in intentional lists, and test that all services handling the request agree.
References
No framework-specific behavior is asserted here. Check the official documentation for the exact framework and request-parsing API used in your application before relying on its handling of repeated keys.
