Research 20 min read Sep 15, 2026 6

When AND Becomes OR: Testing Authorization Logic Before It Becomes a Bypass

Authentication can succeed while a Boolean authorization rule grants too much access. We examine policy composition and tests that vary roles, tenants, and step-up requirements together.

ApplicationSecurity Authorization AccessControl AutonomousPentesting SecureCoding DevSecOps SecurityTesting
S
SecureHup Team
Security Researcher · SecureHup
When AND Becomes OR: Testing Authorization Logic Before It Becomes a Bypass

A payment approval route requires role approver, membership in the invoice's tenant, and recent step-up authentication. The helper receives all three conditions but evaluates them with ANY. A finance user in the right tenant satisfies membership alone and approves the payment. Authentication, session validation, and the visible guard all worked; the Boolean contract did not.

Most production policies combine role, permission, tenant, ownership, entitlement, authentication strength, resource state, and risk. Happy-path tests cover the row where everything is true. One denial test covers the row where everything is false. The dangerous rows are in between, where one valid fact accidentally substitutes for another required fact.

Turn the intended policy into a truth table or machine-readable oracle, provision principals for each meaningful partial state, and inspect the protected effect after every request. Code-aware automation is useful here because it can execute the matrix consistently; humans still own the policy semantics the matrix represents.

Key Takeaways

  • Authentication success does not prove authorization correctness. A valid session can receive an invalid privilege decision.
  • Multi-condition helpers are security-sensitive boolean programs. Their meaning must be explicit, testable, and fail closed.
  • Positive tests are insufficient. The most valuable cases satisfy every proper subset of the required conditions and confirm that each is denied.
  • Code-aware agents can extract policy predicates, build a role and state matrix, execute negative combinations, and produce replayable evidence.
  • A 403 alone is not a complete oracle. Tests must also check data access, state changes, queued jobs, audit events, and other side effects.
  • The durable fix is a closed loop from invariant to code, runtime proof, remediation, and regression replay.

Authorization Is a Boolean Program

Developers often read an authorization call as if its syntax were self-evident:

await requireAccess({
  permission: "workspace:delete",
  plan: "enterprise",
  assurance: "step-up"
});

A reviewer will probably interpret this as:

ALLOW = has_permission
    AND has_plan
    AND completed_step_up

But the runtime implementation may do something different. It may partition attributes into separate groups and use some() where it needs every(). It may short-circuit after a billing entitlement succeeds. It may drop authorization fields while parsing redirect options. It may treat an unsupported predicate as absent. It may catch an internal evaluation error and return the authenticated state as a fallback.

The call site remains visually convincing because the security bug lives below it. The application developer wrote three conditions. The helper accepted three conditions. No type error occurred. Authentication worked. Logs showed an identified user. Only the boolean meaning was wrong.

That distinction matters because security reviews often use the presence of a guard as a proxy for enforcement. A route wrapped by an authorization function looks safer than an unguarded route, so reviewers spend their time elsewhere. For compound policies, the important question is not whether a guard exists. It is whether the guard implements the intended truth table.

The Truth Table Exposes the Bug

Assume a destructive action requires three facts:

  • P: the principal has the delete permission.
  • E: the organization has the required entitlement.
  • R: the principal recently completed step-up authentication.

The expected decision is P AND E AND R.

P E R Expected decision Why the case matters
false false false Deny Baseline unauthorised state
true false false Deny Permission must not override entitlement and assurance
false true false Deny Paying for a feature must not grant an operational role
false false true Deny Strong authentication does not create permission
true true false Deny Step-up is an independent requirement
true false true Deny Entitlement is an independent requirement
false true true Deny Permission is an independent requirement
true true true Allow The only valid combination

Many test suites cover only the first and last rows. Those rows prove that a completely unauthorized user is denied and a fully authorized user is allowed. They do not distinguish AND from several faulty expressions. An implementation such as P OR E OR R passes both tests. So can more subtle grouped expressions such as P AND (E OR R).

The six partial combinations are where semantics become visible. A secure test strategy treats them as first-class cases, not optional edge cases.

The Security Invariants Behind the Syntax

An authorization policy should be reducible to explicit invariants. Without them, a test runner can observe behavior but cannot determine whether that behavior is correct.

For compound checks, five invariants are especially useful.

1. Complete Conjunction

If the policy declares several mandatory predicates, access is allowed only when every predicate is true. Missing, unknown, or unevaluable requirements are failures, not neutral values.

Formally, for required predicates r1 ... rn:

allow(principal, action, resource, context) = true
only if every required ri evaluates to true

2. No Privilege From Irrelevant Success

A successful predicate in one dimension must not compensate for failure in another unless the policy explicitly defines alternatives. A product plan can expose a feature to an organization, but it cannot establish that a particular member may perform an administrative action. Recent MFA can strengthen confidence in identity, but it cannot create object ownership.

3. Deny on Ambiguity

Unknown roles, stale entitlement data, policy-service timeouts, malformed context, and unsupported condition types must not silently become permission. Availability decisions are legitimate architecture choices, but a fallback that converts evaluation failure into allow is a privilege decision and must be treated accordingly.

4. Context Binding

Every predicate must refer to the same decision context. The role must belong to the active tenant. The entitlement must apply to that tenant. The step-up event must belong to the same principal and session. The object must be owned by, shared with, or delegated to that tenant according to policy. Independently valid facts can still be invalid when combined across contexts.

5. Decision and Effect Must Agree

A denied request must not perform the protected operation. This sounds obvious, but asynchronous jobs, event publishing, cache mutation, and deferred writes can happen before a controller returns an error. The observable HTTP response is only one part of the authorization result.

These invariants provide a stable contract even when frameworks, helper APIs, and policy engines change. They also give an autonomous test system a defensible oracle: a way to label observed behavior as expected or exploitable.

Why Conventional Testing Misses Compound Authorization Bugs

Authorization failures survive because several testing habits align in the wrong direction.

Happy Paths Dominate Feature Tests

Feature tests usually create an administrator, grant the necessary plan, call the endpoint, and expect success. The setup bundles all privileges together because that is convenient. It confirms functionality but provides almost no evidence about policy boundaries.

Negative tests often use an anonymous request. That verifies authentication, not authorization. Even a basic authenticated user with no privileges tests only the all-false row. The dangerous cases are partially privileged users whose state resembles legitimate production accounts.

Unit Tests Mirror the Implementation

Tests may construct inputs using the same helper abstractions as production code. If both the test fixture and the guard share an incorrect assumption, they agree with each other. A useful security oracle should be expressed independently, such as a policy table or invariant specification, rather than derived solely from the function under test.

Static Tools Recognize Guards, Not Meaning

Static analysis can identify missing middleware, unsafe sinks, or calls to sensitive functions. It is much harder to infer the intended semantics of a framework-specific options object, especially when policy is assembled dynamically. The tool may see an authorization call without knowing whether its fields should be conjunctive, disjunctive, tenant-bound, or conditional on resource state.

Dynamic Scanners Lack Principals and State

A scanner with one account cannot test a multi-dimensional policy. It needs identities with controlled roles, plans, permissions, ownership, and authentication age. It also needs to know which differences are intentional. Without that model, swapping tokens produces ambiguous results and risks touching real data.

Reviews Stop at the Route Boundary

A reviewer may confirm that the endpoint calls a guard but not inspect the helper implementation, adapter version, argument normalization, or downstream enforcement. This is particularly risky when an upgrade changes semantics without changing the call signature.

The solution is not simply to add more random test requests. It is to connect policy intent, code structure, controlled state, and runtime evidence.

From Code to a Testable Policy Model

A code-aware autonomous test begins before sending traffic. It creates an impact graph from the repository and deployment context.

Step 1: Locate Enforcement Points

The system maps routes, resolvers, command handlers, background jobs, and service methods to authentication and authorization controls. It should identify both explicit calls and inherited middleware. It also records sensitive effects such as database writes, secret retrieval, export generation, permission changes, and external actions.

Step 2: Normalize Policy Expressions

Framework syntax should be translated into a small internal policy language. For example:

action: workspace.delete
resource: workspace
effect: destructive
required:
  all:
    - permission: workspace.delete
    - entitlement: enterprise
    - assurance: step_up_recent
bindings:
  - principal.tenant_id == resource.tenant_id

This representation is not meant to replace application policy. It gives the test engine a clear expectation that can be reviewed, versioned, and compared with runtime behavior.

Step 3: Separate Evidence From Assumption

The agent should annotate how each rule was inferred. A predicate explicitly declared in policy code has stronger evidence than one guessed from a route name. Documentation, tests, configuration, ownership metadata, and prior findings can add confidence. Low-confidence inferences should become review questions, not automatic merge blockers.

Step 4: Build the Principal-State Matrix

The test environment creates synthetic principals representing meaningful combinations. For the earlier example, that means users with every proper subset of permission, entitlement, and step-up status. When tenant and object ownership matter, those dimensions are added deliberately rather than multiplied without limit.

Step 5: Define the Effect Oracle

For each action, the model records what success and denial mean. A delete action may require checking the response, row existence, tombstone state, audit event, emitted message, and scheduled cleanup job. A read action may require verifying both status and response fields. An export action may return 202, so the decisive evidence is whether a job or downloadable artifact was created.

This preparation turns autonomous testing into policy verification instead of blind fuzzing.

An Autonomous Pentest Workflow for Authorization Composition

The workflow should be bounded, deterministic where possible, and explainable to developers.

Stage Agent activity Required guardrail Output
Discover Map protected operations and authorization calls from code and API contracts Stay within approved repository and route scope Enforcement and effect graph
Model Convert compound checks into explicit predicates and bindings Mark inferred rules with confidence and require review for ambiguity Versioned policy model
Provision Create synthetic tenants, principals, roles, entitlements, and session states Never use customer accounts or production data Controlled fixture manifest
Generate Produce full or risk-prioritized negative combinations Cap case count and avoid meaningless Cartesian expansion Executable test plan
Execute Call the protected operation under each state Use isolated environment, rate limits, and non-destructive substitutes Request and response traces
Observe Check databases, queues, files, events, and audit records Collect only test-owned artifacts Side-effect evidence
Decide Compare observations with the independent policy oracle Do not infer exploitability from status code alone Pass, fail, or needs-review result
Preserve Redact and package the minimum reproduction Exclude secrets and unrelated data Replayable finding artifact
Retest Run the same case against the fix and future relevant changes Keep fixture and oracle stable Closed regression record

Coordinating Multiple Test Agents

Different agents can specialize without acting independently against the same state. One agent can analyze code and extract predicates. A second can provision principals. A third can execute requests. A fourth can inspect side effects and assemble evidence. The orchestration layer should serialize state-changing cases or isolate each case in its own fixture to prevent one result from contaminating another.

Autonomy is valuable when it expands systematic coverage. It becomes dangerous when agents invent scope, use uncontrolled identities, or interpret ambiguous business behavior as a vulnerability without review. The policy model and fixture manifest are therefore part of the authorization boundary for the test system itself.

Designing Negative Tests That Prove the AND

For a policy requiring n independent conditions, exhaustive testing produces 2^n combinations. That is practical for three or four predicates but expensive for a large policy. We can combine exhaustive and property-based strategies.

Test Every Proper Subset for Critical Actions

For destructive, administrative, financial, or cross-tenant operations, test all proper subsets of mandatory conditions. If four conditions are required, all 15 non-complete combinations should deny. This is the clearest way to detect accidental OR, grouping mistakes, and dropped fields.

Use Boundary Cases for Larger Policies

When the policy has many predicates, prioritize:

  • All conditions true except one, repeated once per condition.
  • Exactly one condition true, repeated once per condition.
  • Conditions grouped by implementation branch.
  • Unknown, missing, malformed, expired, and stale values.
  • Facts from mismatched tenants, sessions, or resources.
  • Cached and freshly evaluated variants.

The first group is especially powerful. If access requires eight conditions, eight cases where exactly one is false prove that every condition is independently necessary.

Add Metamorphic Properties

Metamorphic tests assert relationships between decisions rather than hard-coding every expected output. Useful properties include:

  1. Removing a required privilege from an allowed state must not preserve allow.
  2. Adding an unrelated entitlement must not turn a denied role into an allowed role.
  3. Moving an otherwise valid role to another tenant must cause denial.
  4. Expiring the step-up window must cause denial for actions that require it.
  5. Replacing a known role or permission with an unknown value must not grant access.
  6. An evaluation error must not produce a more permissive result than a successful evaluation.

These properties can expose bugs even when the agent does not know every business-specific role name.

Example Regression Test

The following pseudocode keeps the expected policy separate from the runtime helper:

for (const state of properSubsets(["permission", "plan", "stepUp"])) {
  const fixture = await createIsolatedWorkspace(state);
  const before = await snapshotEffect(fixture.workspaceId);

  const result = await invokeDelete(fixture.session, fixture.workspaceId);
  const after = await snapshotEffect(fixture.workspaceId);

  expect(result.status).toBeOneOf([401, 403, 404]);
  expect(after).toEqual(before);
}

The exact denial status is application policy. The invariant is that no unauthorized effect occurs. A 404 may intentionally hide resource existence; a 403 may be appropriate for an authenticated principal. Tests should verify the chosen contract consistently without assuming one universal response.

Evidence: What a High-Quality Finding Must Contain

An authorization finding should prove a policy violation without exposing unrelated data. A strong evidence package contains:

  • The protected action and code location.
  • The intended policy in plain language and normalized form.
  • The synthetic principal's relevant attributes, with tokens and secrets redacted.
  • The exact predicates satisfied and unsatisfied.
  • A minimal request trace or test invocation.
  • The observed decision.
  • Before-and-after evidence for the protected effect.
  • The expected decision and why it follows from the policy.
  • The smallest known root-cause location or dependency boundary.
  • A replay identifier and deterministic fixture instructions.

Severity should follow impact, not the visual oddity of the code. A compound-policy bug on a profile-color endpoint is different from the same bug on tenant deletion, billing administration, secret access, or identity-provider configuration. The agent can collect technical proof, but business impact still requires knowledge of the action and affected data.

Evidence also prevents a common failure mode in automated security: reporting a theoretical branch without proving reachability. If the vulnerable helper exists but the application uses only single-condition checks, the code may be affected at the dependency level without an exploitable application path. The finding should distinguish those states.

Remediation: Fix the Contract, Not Only the Symptom

When a framework or library has corrected authorization semantics, upgrading to a patched release is the primary action. A local workaround can reduce exposure, but it should not become a reason to remain on a vulnerable dependency indefinitely. Teams should review the current maintainer advisory for exact package and version guidance because framework wrappers may consume the affected core transitively.

The application still needs its own defenses.

Make Composition Explicit

Prefer APIs that reveal allOf, anyOf, and not semantics. Avoid overloaded option objects whose combination rules exist only in documentation. If conditions are all mandatory, independent sequential checks can be safer as a temporary measure because each one must pass before execution continues.

Use Typed, Closed Policy Inputs

Unknown policy fields should fail validation. A typo in permission, an unsupported assurance mode, or a stale feature name should not be silently ignored. Types help at compile time, while runtime schema validation protects dynamic configuration and cross-service inputs.

Centralize Decision Logic Without Hiding It

A consistent policy layer reduces scattered checks, but centralization is not enough. The layer needs observable decisions, versioned policy semantics, and tests that do not simply restate its implementation. Route-level wrappers should preserve every authorization parameter when they also handle redirects, token types, or error behavior.

Keep Authentication and Authorization Separate

An authenticated fallback must never stand in for an authorization decision. Code should represent at least three states: unauthenticated, authenticated but unauthorized, and authorized. Evaluation errors should be distinguishable from both and should follow an explicit fail-closed policy for sensitive actions.

Test Effects Transactionally

Authorization should happen before irreversible work. Where practical, resolve policy and resource context before opening side-effecting transactions or publishing events. If the action spans systems, use idempotency and compensating controls, but do not treat compensation as permission to execute before authorization.

CI/CD: Turning One Bypass Into a Permanent Regression Test

A verified authorization bypass should not end as a closed ticket. It should become a durable security test tied to the relevant code and policy dimensions.

On each pull request, a semantic diff can determine whether the change touches:

  • Authorization helpers or policy configuration.
  • Routes that use the affected policy.
  • Role, permission, plan, or feature definitions.
  • Session assurance and reauthentication logic.
  • Tenant or object binding.
  • Dependency versions that implement the guard.

Relevant changes trigger the focused matrix rather than a full application pentest. The fastest layer can run policy unit and property tests. A second layer deploys an isolated environment and replays verified exploit cases. Broader scheduled testing can explore combinations not directly linked to the diff.

A merge gate should block when a deterministic, high-confidence case proves that a denied state causes a protected effect. It should not block merely because an agent inferred an undocumented policy or because a dependency advisory exists without a reachable call shape. Those conditions require review or remediation tracking, but they are different evidence states.

Suggested Result States

State Meaning Merge treatment
verified_bypass Runtime behavior violates a reviewed policy and produces a protected effect Block
regression_reproduced A previously fixed exploit works again Block
affected_pattern_reachable Vulnerable call shape is reachable, but impact proof is incomplete Escalate for rapid review
dependency_affected_only Affected version exists, but no vulnerable use was found Upgrade and track according to risk
policy_ambiguous Expected behavior cannot be established from approved sources Do not label as a bypass; request policy decision
not_reproduced Controlled cases deny and effects remain unchanged Record coverage, not proof of universal safety

This vocabulary keeps automation honest. Failure to reproduce one path does not prove that no bypass exists, while a library version match does not prove application exploitability.

Operational Review Checklist

Teams can use the following checklist when introducing or changing compound authorization:

  1. Write the intended decision as an explicit boolean expression.
  2. Identify principal, tenant, resource, action, environment, and session context.
  3. Distinguish mandatory conditions from valid alternatives.
  4. Define behavior for missing, unknown, stale, and failed evaluations.
  5. Confirm that every predicate binds to the same principal and tenant.
  6. Test the fully allowed case and every critical proper subset.
  7. Include cross-tenant and wrong-owner cases where resources are involved.
  8. Observe side effects, not only HTTP status.
  9. Test all wrapper forms, overloads, and option combinations used by the application.
  10. Record dependency versions and transitive framework packages.
  11. Preserve a minimal, redacted replay artifact for every verified bypass.
  12. Trigger focused replay when policy code, protected routes, or relevant dependencies change.
  13. Block merges only on reviewed policy and high-confidence runtime evidence.
  14. Monitor authorization evaluation errors and unexpected allow decisions in production.
  15. Re-run the matrix after role, entitlement, assurance, or tenancy model changes.

Metrics That Show Whether the Control Works

Counting authorization checks does not tell us whether they are correct. Better measures include:

  • Percentage of sensitive operations linked to a reviewed policy model.
  • Percentage of mandatory predicates with an independent negative case.
  • Coverage of role, tenant, ownership, entitlement, and assurance dimensions.
  • Number of verified bypasses versus speculative authorization alerts.
  • Median time from policy-affecting diff to focused test result.
  • Percentage of fixed authorization findings with passing replay tests.
  • Rate of policy evaluation errors and fail-closed outcomes.
  • Number of ambiguous policies awaiting an owner decision.

These metrics encourage clarity and regression resistance. They do not reward teams for generating large volumes of low-confidence findings.

FAQ

Is a multi-condition authorization helper always supposed to use AND?

No. Some policies legitimately allow alternatives, such as an organization owner or a delegated security administrator. The requirement is explicit composition. The caller, implementation, tests, and reviewers must agree on whether conditions are mandatory, alternative, or nested. Ambiguous option-object semantics are the problem.

Can SAST detect an AND-to-OR authorization bug?

It can detect some local patterns, especially a direct use of the wrong boolean operator. It is less reliable when semantics are hidden in framework helpers, dynamic policy data, overload resolution, adapters, or transitive dependencies. Runtime matrix testing is needed to prove the effective decision.

Why not test only one unauthorized role and one administrator?

Those cases usually represent all-false and all-true predicate states. Many incorrect boolean expressions produce the same result for both. Partially privileged states are necessary to prove that every mandatory condition is independently enforced.

Should authorization tests run against production?

State-changing and combination-heavy tests should normally run in isolated environments with synthetic data. Carefully bounded, non-destructive production checks may validate selected controls, but they require explicit authorization, scope, rate limits, monitoring, and a kill switch. Production customer data should not be used as a test fixture.

What should an autonomous pentest agent do when policy intent is unclear?

It should stop short of declaring a vulnerability. The agent can show the observed behavior, code evidence, competing interpretations, and affected operation, then request a decision from the policy owner. Once intent is approved, it becomes the oracle for testing and future replay.

Policy Intent Needs an Executable Form

A truth table turns an informal authorization sentence into predicates, combination rules, missing-context behavior, and an expected protected effect. It also exposes the partial rows that ordinary happy-path and total-denial tests tend to miss. Controlled principals can then show whether durable state, queued work, notifications, and audit records agree with the intended result.

We would retain any confirmed semantic failure as a regression connected to both the helper and its callers. Where the allowed case genuinely depends on every condition, the remaining rows should create no protected effect. That is stronger evidence than the presence of a familiar authorization wrapper whose Boolean meaning no one has tested.

#ApplicationSecurity #Authorization #AccessControl #AutonomousPentesting #SecureCoding #DevSecOps #SecurityTesting

Was this helpful?
Share :

Find issues like this in your own codebase

SecureHup runs this exact exploit chain — discovery, enumeration and cross-tenant validation — on every deployment, automatically.