Query-param smuggle-rejection

Read endpoints should reject any query parameter they do not explicitly bind.

Otherwise a client could "smuggle" a parameter such as ?tenantId= in an attempt to override server-resolved context. QueryParamGuard blocks this, and OrderFieldsGuard (or the static OrderFields) does the same for the orderByField value.

QueryParamGuard

The guard derives its allow-list once, from the @QueryValue properties of the introspected query beans you pass it. There is no hand-maintained string list to drift: adding a field to a query bean automatically widens the allow-list.

private final QueryParamGuard queryParamGuard = QueryParamGuard.fromBeans(Paging.class, WidgetQuery.class);

Capture every incoming parameter with a catch-all binding and hand the map to the guard:

@Get("{?queryParams*}")
public SliceEnvelope<Widget> list(
    @RequestBean Paging paging,
    @RequestBean WidgetQuery query,
    @Nullable @QueryValue Map<String, String> queryParams
) {
    queryParamGuard.rejectUnknown(queryParams);
    // ...
}

Any parameter whose head segment (before a .) is not in the allow-list is rejected with a HttpProblemCode.UnsupportedQueryParameter 400. The problem carries the offending parameter name and the allowed set.

Order-field validation

The orderByField value is a parameter value, not a parameter name, so QueryParamGuard does not validate it. Restrict it to a known set of sortable columns.

The instance style mirrors QueryParamGuard — build the guard once with the allow-list, then validate per request:

private final OrderFieldsGuard orderFieldsGuard = OrderFieldsGuard.fromFields(WidgetOrderField.allowedNames());
// ...
orderFieldsGuard.requireAllowed(paging.getOrderByField());

The static OrderFields.requireAllowed is the one-off equivalent, taking the allow-list at the call site:

OrderFields.requireAllowed(paging.getOrderByField(), WidgetOrderField.allowedNames());

A null field means "no ordering" and is allowed. Anything outside the allow-list is rejected with a HttpProblemCode.UnsupportedOrderField 400.

The idiomatic allow-list is an @Introspected enum of permitted fields:

@Introspected
public enum WidgetOrderField {

    createdAt,
    updatedAt,
    name;

    private static final Set<String> NAMES = Arrays.stream(values())
        .map(Enum::name)
        .collect(Collectors.toUnmodifiableSet());

    public static Set<String> allowedNames() {
        return NAMES;
    }
}

Problem codes

HttpProblemCode provides the stable, machine-readable codes raised by these guards. They are standard codes (STD- prefix, 2xxx sub-range) extending conta-problem-json.

Constant Code Status Raised by

UnsupportedQueryParameter

STD-2000

400

QueryParamGuard.rejectUnknown

UnsupportedOrderField

STD-2001

400

OrderFieldsGuard / OrderFields.requireAllowed

Rendering these as application/problem+json requires conta-problem-json-mn in the consuming application — see Getting started.