How do you keep a negative test from passing without proving the control works?
answer
- green by default, for many wrong reasons
- prove the target existed
- a positive control in the same run
- assert the design's refusal, plus state and audit
- remove the guard; it must go red
basics
~20 sPair every negative assertion with a positive control in the same run, assert the exact refusal the design chose plus unchanged state and a denial record, and confirm the test fails when the guard is disabled.
solid answer
~50 sA negative test is green whenever the attempt does not succeed — including when it never really happened. The target record was never seeded, the payload was malformed, the environment lacked configuration and returned a server error. All refusals, none of them evidence. Three habits fix it. First, a positive control in the same run: the legitimate party performs the same operation and succeeds, which proves the fixture, the route and the data are real. Second, assert the refusal the design actually specified — the chosen status and error code — plus the invariant and the denial audit event, rather than "not a success". Third, once, locally, disable the check and confirm the test goes red; a negative test that stays green with the guard removed is decoration. In an e-signature service: the unauthorised link holder is refused, the envelope stays unopened, and the denial is audited.
go deeper
Learn that a test which only checks "this did not work" can be green for reasons unrelated to security. Ask what the test would do if the data it targets had never been created.
Be able to list the vacuous-pass causes — missing fixture data, malformed request, broken environment, wrong principal — and explain what a positive control in the same run rules out.
Demonstrate the discipline you apply on real suites: assert the specified refusal plus unchanged state plus a denial record, verify the test fails when the check is disabled, and avoid brittle message assertions.
Own what a green negative suite is allowed to mean to the organisation, and set the bar for evidence — which threats require a proven-to-fail assertion before they can be considered handled.
## The failure this question is about Negative tests pass by default. Anything that stops the operation from succeeding turns the test green, and most of those things have nothing to do with your control. This is the security-testing equivalent of a test that asserts nothing, and it is worse than no test, because a green suite is read as coverage and the threat gets marked handled. The ways a negative test passes vacuously, roughly in order of how often they happen: - **The target does not exist.** The abuse case is "read someone else's document" but the fixture never created that document, so the attempt gets a not-found and the test is green against an empty database. - **The attempt was malformed.** A schema or type error rejects the request before it ever reaches the authorization check. - **The environment is broken.** A missing configuration value produces a server error on every call, and the assertion "not successful" is satisfied by a service that is simply down. - **The fixture principal is wrong.** The test authenticates as nobody, or as a user with no roles at all, so a coarse gate refuses it long before the specific control you meant to test runs. - **The wrong control did the work.** A rate limiter, a proxy rule, or a request-size cap refused it, and the authorization logic you believed you were exercising was never invoked. ## Worked case — an e-signature envelope A document e-signature service. Abuse case: an authenticated user of the service holds a signing link addressed to a different recipient and walks it to open the envelope. The asset is non-repudiation and audit truth — who signed what, and the ability to prove it later. A weak test authenticates as some user, requests the envelope, and asserts the response was not a success. It goes green in an environment where the envelope was never created, and it stays green if the authorization check is deleted next sprint. A strong test does four things: 1. **Seeds the envelope and proves it.** The rightful recipient opens it successfully in the same test run. This is the positive control, and it kills the empty-database class of vacuous pass in one line. 2. **Asserts the specified refusal.** Whatever the design chose — an explicit forbidden response with a documented error code, or a deliberate not-found to avoid disclosing that the envelope exists — the test asserts *that* decision, so a change in behaviour is a test failure and a design conversation rather than a silent drift. 3. **Asserts the invariant and the evidence.** The envelope is still unopened, no signature is recorded against it, and the audit log contains a denial event naming the attempting principal and the envelope. For this asset the audit assertion is not optional garnish: an unrecorded denial is itself a finding. 4. **Is proven to be able to fail.** Disable or remove the authorization check locally and run it; it must go red. This is a one-off manual verification, cheap and decisive, and it is the answer interviewers are listening for. ## Assert the design's decision, not your preference Candidates often insist the response must be a forbidden status. Sometimes the design deliberately returns not-found so an unauthorised caller cannot learn that a resource exists. Both are legitimate; the test's job is to encode whichever one the model decided, together with the compensating assertions — unchanged state and a denial record — that stop the ambiguous status from hiding a real failure. What is never acceptable is treating a server error as proof the attack was blocked. A crash is an availability finding, not a passing control. ## Brittleness, the opposite mistake Over-correcting produces tests that assert exact message strings, response ordering, or internal identifiers. Those fail on copy edits and teach the team to ignore the negative suite, which is a slower path to the same place. Assert status plus a stable machine-readable error code, plus state and evidence. That is specific enough to catch a regression and stable enough to survive a refactor. ## One environment is not enough A negative test that holds in one place can fail in another, because the thing that re-opens the threat is frequently configuration rather than code — a permissive setting, a stale policy binding, a feature flag left on. A multi-region loyalty API makes the point: the assertion "a member cannot read another member's points ledger" is written once and then run against every region and every environment as a standing check. Same test, many places, because that is where the drift lives. ## What to say in an interview Name the vacuous-pass classes, describe the positive control, describe asserting the specified refusal plus invariant plus evidence, and finish with the guard-removal check. Mentioning that you have personally seen a negative suite go green against an empty database is the detail that reads as lived experience.
- The design deliberately returns not-found instead of forbidden so an attacker cannot learn the resource exists. How do you still assert the control?Assert the not-found, because that is the decision the model made, and add the assertions that disambiguate it: a positive control in the same run proving the resource does exist for its rightful owner, the resource still untouched afterwards, and a denial event in the audit log naming the attempting principal. Together those make an ambiguous status a meaningful assertion.
- How do you stop a negative test from being satisfied by an empty database?Seed the target explicitly and prove the seed with a positive control in the same test run. If the rightful party can read or act on the record and the adversary cannot, the refusal is about authorization. Without that pairing you cannot distinguish "the control refused you" from "there was nothing there".
- Is a negative test passing once, in one environment, enough to call the threat closed?No. The common way a closed threat re-opens is configuration rather than code — a permissive setting, a stale binding, a flag. The same assertion should run against every environment and region where the service is deployed. That is why a check like "a member cannot read another member's ledger" is written once and treated as a standing deliverable rather than a release-day activity.
- Why is a server error an unacceptable pass condition for a negative test?Because it proves nothing about the control and hides two separate problems. The operation may have failed before the authorization check ran, so the guard is untested, and an attacker-triggerable crash is itself an availability finding. The test should assert the specific refusal and treat a server error as a failure of the run.
A negative test that passes because the record was never seeded is a lock that seems to hold because nobody ever hung a door in the frame.
saying these in an interview costs you the question
- Asserts only that the response was not a success
- Accepts a server error as proof the attack was blocked
- Runs against a fixture where the target record was never created
- Pins the exact error-message wording instead of a stable code
- Never checks the test fails when the guard is removed
- Skips the denial audit assertion when audit truth is the asset