Why is a tenant admin token in a Cypress `expose` block a leak, and what does moving it cost?
answer
- One block is a publication channel
- The page is shared with other code
- Extensions and devtools count too
- The replacement is a command, not a function
- Wrap the read in a custom command
basics
~20 sEverything in the expose block is deliberately browser-visible: application code, third-party scripts and extensions on the page can read it. Moving the token to cy.env() fixes that, but every read becomes an asynchronous command, so synchronous module-scope reads must be restructured.
solid answer
~50 s`Cypress.expose()` exists to put values *into* browser state, which is exactly what a tenant admin token must not be. A staging admin console usually carries analytics, session-recording and tag-manager scripts, and the browser profile may carry extensions; all of them run in that context and can read an exposed value, as can anyone with devtools. The right home is the `env` block, read with `cy.env(['tenantAdminToken'])`, which keeps the value in Node and crosses only the keys a test names. The cost is shape, not security. `Cypress.expose()` is synchronous, so it can be read at module scope, in a support file, or in plugin code. `cy.env()` is a command: it runs only inside a test or hook and yields into `.then()`. Each synchronous read becomes a chained read, a Mocha `before()` hook that captures the value, or a custom command.
go deeper
Remember the rule of thumb: expose is for values you would be happy to print on the page, and cy.env() is for everything else. Credentials never belong in the expose block.
Explain who shares the browser context with an exposed value, and describe the shape change the fix forces: a synchronous function call becomes a queued command that yields into a callback.
Show the whole remediation: find every exposed key including the ones set at runtime, classify them, restructure the reads, and treat the exposed credential as compromised rather than merely misfiled.
Own the incentive. If the safe read is awkward, someone will publish the value to save keystrokes, so invest in the wrapper command and the review habit that keeps the classification honest.
## What `expose` actually promises `Cypress.expose()` and the `expose` configuration block exist for one purpose: to make a value **available in the browser context, synchronously**. That is not a side effect to be managed; it is the feature. The Cypress documentation is blunt about the consequence — exposed values are accessible to application code, third-party scripts and browser extensions — and the best-practice guidance names using `Cypress.expose()` for sensitive values as an anti-pattern outright. So a tenant admin token in the `expose` block is not "slightly less private". It is published to every piece of code sharing that page. ## Who can read it on a staging admin console The staging build of an internal console is usually the *worst* place to make this mistake, because it tends to carry more page-level code than production, not less: - **The application under test.** Its own code runs in the same context and can read exposed values. - **Analytics, tag managers and session-recording scripts.** A session recorder that captures page state will happily capture yours, and ship it to a third party. - **Browser extensions** present in the profile the run uses. - **Anyone with devtools**, including whoever picks up a shared debugging session. - **The Cypress Command Log**, indirectly: an exposed value read into an assertion or a failing command prints like any other value, and a failed run's captured output carries it. Treat a token that has been exposed as compromised and replace it. *How* it gets replaced — storage, rotation, and how the pipeline injects the new one — is a secrets-management question, not a Cypress one; the Cypress-side fix is to change where the value is declared. ## What the move to `cy.env()` changes in your code Moving the key from `expose` to `env` in the configuration file is one line. The refactor on the spec side is the real work, because the two APIs have different **shapes**: | | `Cypress.expose('tenantAdminToken')` | `cy.env(['tenantAdminToken'])` | |---|---|---| | Call style | plain synchronous function | queued Cypress command | | Where it can run | module scope, support files, plugin code, a Mocha `describe` body | inside a test or a hook only | | Result | returned immediately | yielded into `.then()` | | Failure mode of a bad move | value silently public | `cy.env()` outside a running test | Every synchronous read has to become one of three shapes: 1. **A chained read at the point of use.** `cy.env(['tenantAdminToken']).then(({ tenantAdminToken }) => { ... })` — the smallest change, and the one that keeps the value's lifetime shortest. 2. **A Mocha `before()` hook that captures it.** Read once into a variable the tests close over. Cheaper in a spec that needs the value in a dozen places, at the cost of a value that lives longer. 3. **A custom command.** Wrap the read in `Cypress.Commands.add()` so specs call `cy.adminRequest('/tenants/acme-corp/settings')` and never touch the token. This is usually the right end state for a suite of any size, because it makes the safe path the convenient one. ## Finding the ones you have already shipped - Grep the repository for `Cypress.expose(` and for the `expose` block, and classify every key by hand — the API cannot tell a feature flag from a credential. - Watch specifically for keys that were migrated *away* from the removed `Cypress.env()` in a hurry. `Cypress.expose()` is the drop-in that compiles; `cy.env()` is the one that is correct for a secret, and the temptation under a deadline runs the wrong way. - Remember that anything set at runtime with `Cypress.expose(key, value)` lasts only for the rest of that spec file and never propagates back to Node, so a grep of the configuration file alone does not find every exposed key. ## What moving it does not fix Two things survive the move, and a senior answer says so: - **The value is still in Node, and still reaches the browser when you ask for it.** `cy.env()` narrows exposure to the keys you request at the moment you request them; it does not make the value unprintable. Everything about keeping it out of the Command Log after it yields still applies. - **A value that must be both secret and synchronously readable does not exist in this model.** If plugin or support-file code genuinely needs a credential before any test runs, the design has to change — that code moves into a hook, or the work moves into Node — because Cypress 16 offers no API with both properties, and that is intentional.
- Where can `Cypress.expose()` be read that `cy.env()` cannot?Anywhere synchronous: at module scope in a spec, in top-level support-file code, in a Mocha `describe` body, and in plugin code that needs configuration before any test runs. `cy.env()` is a queued command, so it only runs inside a test or a hook. That difference, not security, is what makes the migration a refactor rather than a rename.
- How long does a value set at runtime with `Cypress.expose(key, value)` survive?Only for the remainder of the current spec file, and only in the browser — it never propagates back to Node. That also means grepping the configuration file alone will not find every key a suite exposes, since some are set from spec or support code at runtime.
saying these in an interview costs you the question
- Calls the expose block private because tests are internal
- Thinks a staging environment makes exposure harmless
- Assumes swapping the API is a one-line rename
- Forgets that plugin code cannot call cy.env()
- Leaves the exposed credential in place after fixing the config