Skip to content

Guard against DEV_DISABLE_OAUTH being enabled in production/staging#369

Open
sebastiondev wants to merge 2 commits into
cloudflare:mainfrom
sebastiondev:fix/cwe200-api-token-mode-dev-2d24
Open

Guard against DEV_DISABLE_OAUTH being enabled in production/staging#369
sebastiondev wants to merge 2 commits into
cloudflare:mainfrom
sebastiondev:fix/cwe200-api-token-mode-dev-2d24

Conversation

@sebastiondev
Copy link
Copy Markdown

Summary

This PR adds a runtime guard preventing DEV_DISABLE_OAUTH from being active in production or staging environments. Today, if both DEV_DISABLE_OAUTH=true and DEV_CLOUDFLARE_API_TOKEN are set on a deployed Worker, OAuth is bypassed for every incoming request and any caller is authenticated as the operator-supplied dev token. The change is small (~14 lines) and localized to packages/mcp-common/src/api-token-mode.ts.

I want to be upfront that this is a defense-in-depth / misconfiguration hardening fix, not a directly attacker-triggerable vulnerability — the precondition is operator error. But the blast radius if it ever happens is total auth bypass, so failing fast at startup felt like the right tradeoff.

The issue

In packages/mcp-common/src/api-token-mode.ts:

export async function isApiTokenRequest(req: Request, env: RequiredEnv) {
    // shortcircuit for dev
    if (env.DEV_CLOUDFLARE_API_TOKEN && env.DEV_DISABLE_OAUTH === 'true') {
        return true
    }
    ...
}

When this branch is taken, isApiTokenRequest returns true unconditionally and handleApiTokenMode then authenticates the request using DEV_CLOUDFLARE_API_TOKEN / DEV_CLOUDFLARE_EMAIL. There is no check that the deployment is actually a dev environment. If those env vars ever get copied into a production Worker secrets file (or a staging secrets file gets promoted without auditing), every public request to the MCP server is implicitly trusted.

The wrangler configs for apps/workers-observability, apps/logpush, and apps/dex-analysis all set ENVIRONMENT per-environment (development / staging / production), so a guard keyed on ENVIRONMENT is reliable across the affected apps.

This maps most cleanly to CWE-489 (Active Debug Code) / CWE-1188 (Insecure Default Initialization of Resource) — a dev-only auth shortcut with no environmental gate.

The fix

Add assertDevOAuthNotInProd(env) at both entry points to the dev shortcut (isApiTokenRequest and handleApiTokenMode). It throws if DEV_DISABLE_OAUTH === 'true' and ENVIRONMENT is production or staging:

function assertDevOAuthNotInProd(env: RequiredEnv) {
    if (
        env.DEV_DISABLE_OAUTH === 'true' &&
        (env.ENVIRONMENT === 'production' || env.ENVIRONMENT === 'staging')
    ) {
        throw new Error('DEV_DISABLE_OAUTH must not be enabled in production or staging')
    }
}

Rationale:

  • Fail-closed: if a misconfiguration ever ships, the Worker errors loudly on the first request rather than silently authenticating everyone.
  • Placed at both auth-boundary entry points so there is no path into the dev shortcut that skips the check.
  • No behavior change for development (or unset ENVIRONMENT), so local workflows are unaffected.
  • The downstream truthy env.DEV_DISABLE_OAUTH checks elsewhere in cloudflare-api.ts are post-auth and don't matter once this gate refuses to open.

Tests

Added packages/mcp-common/src/api-token-mode.spec.ts covering:

  • throws when DEV_DISABLE_OAUTH=true + ENVIRONMENT=production
  • throws when DEV_DISABLE_OAUTH=true + ENVIRONMENT=staging
  • allows DEV_DISABLE_OAUTH=true when ENVIRONMENT=development
  • allows DEV_DISABLE_OAUTH=true when ENVIRONMENT is unset
  • both isApiTokenRequest and handleApiTokenMode enforce the guard

Full suite passes locally (99/99).

Adversarial review

Before submitting I tried to talk myself out of this. The honest version: an attacker can't trigger this on their own — it requires an operator to set DEV_DISABLE_OAUTH=true and DEV_CLOUDFLARE_API_TOKEN on a prod/staging Worker. So is it worth a guard? I think yes, because (a) those env vars are a plausible copy-paste mistake when promoting configs, (b) there is currently no signal at all when the bypass is active in prod — the server just works, with no auth, and (c) the failure mode is unauthenticated access to all configured Cloudflare API operations under the operator's token. A startup-time hard error is a cheap way to make the misconfiguration impossible to ship silently. Happy to drop or rescope this if you'd prefer a warning log instead of a throw, or a different env-var name convention.

cc @lewiswigmore

…environments

Add a runtime assertion that throws if DEV_DISABLE_OAUTH is set to "true"
while ENVIRONMENT is "production" or "staging". This prevents accidental
misconfiguration from bypassing OAuth and granting unauthenticated access
via the dev API token.

The guard is added to both isApiTokenRequest() and handleApiTokenMode()
as defense-in-depth, since either function could be called independently.

CWE-200: Information Exposure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant