A form check carried more authority than it needed
A public form usually gives a visitor one job: supply a value. The application chooses the validation rule and decides what answer the visitor should receive.
Here, an internal helper was reachable through a generic remote dispatcher. The source notes describe the caller influencing the rule, not just the value. That made an implementation convenience part of the public interface.
The answers exposed the difference. Database-backed checks distinguished supplied candidates that were present from those that were absent. Some debug failures also revealed backend structure. These were two findings to reason about separately: confirmation about stored data, and diagnostic information that shouldn't have been public.
The visitor could influence the question, too
There were two decisions to take back from the caller. First, which actions are public? Second, which rules does each action apply? The dispatcher was too broad, and the helper accepted too much control over its work.
Removing a button from the form would not restrict the dispatcher. Restricting action names would help, but would still leave a problem if an allowed action accepted its validation policy from the request.
This is also a maintenance concern. Internal helpers gain methods or inherit behavior during ordinary refactors. A fixed public action list keeps those changes from quietly expanding what an anonymous caller can reach.
A yes or no needed a careful reading
The result confirmed or rejected a value that had already been supplied. It didn't produce unknown values or demonstrate a database dump or account compromise. The report needs that distinction even when the confirmed fact is sensitive.
A public availability check and a private account-presence check can both return true or false internally. That doesn't make them equally appropriate for an anonymous visitor. Ask whether the caller should learn the fact, not just whether the response looks small.
The debug output is the obvious cleanup task because it looks wrong. Removing it would address the structural leakage, but a quiet boolean could still reveal the underlying fact. Both response detail and the permitted question need review.
The HTTP notebook
The source described backend details appearing in failure responses. The notebook below instead illustrates the proposed public error contract: a generic outcome and a redacted diagnostic reference, without internal names or data.
Reconstructed for this article, not an original wire capture. The host uses the reserved .example namespace; paths, headers, and data are invented or redacted. Display only: no requests are sent.
This is proposed response shaping, not an observed fix. Hiding diagnostic detail alone would not remove the underlying data oracle; the server must also own the callable actions and validation policy.
Return the choice of rules to the application
For the proposed repair, register the public actions explicitly and give each a fixed input schema. Reject unknown or inherited helper actions before validation or database work. Accept values from the request, not rule definitions. Put privileged checks behind a separate operation that verifies permission on the server.
This illustrative pseudocode uses a fixed map of application-selected handlers. The validator receives the schema from that map, never from the caller.
# Synthetic, server-owned public contract.
PUBLIC_ACTIONS = frozen_map(
PUBLIC_FORM_CHECK: action(
schema = FIXED_PUBLIC_FORM_SCHEMA,
handler = check_public_form_values
)
)
handle_public_input(envelope):
entry = PUBLIC_ACTIONS.lookup_exact(envelope.action)
if entry is absent:
audit_unexpected_action_without_input_values()
return GENERIC_PUBLIC_FAILURE
if not values_only_envelope(envelope):
return GENERIC_PUBLIC_FAILURE
try:
values = validate_values(envelope.values, entry.schema)
result = entry.handler(values)
return public_projection(result)
catch validation_or_internal_failure:
record_private_diagnostic_without_input_values()
return GENERIC_PUBLIC_FAILURE
The example checks permitted form input without querying private account presence. It also constructs a public response deliberately instead of returning an internal result object wholesale. Keep diagnostic detail in access-controlled logs and omit submitted values unless there is a specific, justified need to retain them.
Give the component's database account only the operations it needs. That limits future mistakes; it doesn't replace the action list or fixed schemas. Log unexpected action attempts with bounded metadata, retention limits, and access controls so the logs remain useful without becoming another source of exposure.
Count the database calls
A rejected response is only part of the assertion. Use the fixed dispatcher, synthetic values, a fake repository, and a database-call counter to check that disallowed work never starts. These five local tests run in process, not against an exposed service.
| Case | Fixture and local action | Expected state |
|---|---|---|
| 1. Explicit actions only | Pass synthetic unknown and inherited-action descriptors into the fixed dispatcher's unit-test boundary. | Both are denied before validation or database access. The database-call count remains zero. |
| 2. Server owns policy | Add a synthetic policy object to an otherwise ordinary local form envelope. | The envelope is rejected, the static schema remains unchanged, and no extra capability runs. |
| 3. Intended public behavior | Submit permitted synthetic values to the explicitly registered public form action. | The fixed schema validates them and returns the documented public result without private presence checks. |
| 4. Uniform public failures | Have local dependencies raise different synthetic validation and internal failures. | Public status and response shape remain consistent and contain no internal names or structural details. |
| 5. Privileged checks stay protected | Call the separate account-presence service with authorized and unauthorized synthetic principals. | Permission is checked first. Only the authorized principal receives the permitted result; unauthorized calls never reach the repository. |
A small answer can still cross a large boundary
The form only needed to validate input. The helper exposed more than that job required. A repair should make the public action, accepted values, permission check, and returned information explicit enough to review independently. The tests above describe that contract; they are not evidence that a fix was deployed or retested.
For a different example of private information escaping through another layer, read when the cache forgot who a response belonged to. The practical guides cover scoping and reporting these kinds of checks.
What can your public interface reveal?
Public forms, remote actions, and response data are part of our API penetration testing. Tell us what your interface exposes and which operations should remain private.
Request a pentest