E11090: reveal() on Sensitive is only permitted in a method of a class or trait that is Privileged: it has the Privileged trait, a trait that has it, or a super class that has it

Category: E11xxx Code quality and IR analysis · Reported in phase: PRE_IR_CHECKS · Classification: PRIVILEGED_ACCESS_REQUIRED · All error codes

What went wrong

A call to reveal() on a Sensitive value was detected outside a Privileged class or trait. The reveal() method returns the raw secret string and is restricted to methods of a class or trait that is Privileged.

Privilege is inherited. A class or trait is Privileged when it has the Privileged trait itself, has a trait that has it anywhere in its trait chain, or extends a class that is Privileged. So 'Derived extends Base', where 'Base with trait of Privileged', may call reveal() too.

Worked examples

Compilable examples demonstrating this error and the correct fix:

  • ek9 -q 703 (How do I access the raw value of a Sensitive in EK9?)
  • ek9 -q 706 (How do I use credentials, URLs, usernames and passwords o...)

Rationale

The Sensitive type wraps secret values (API keys, passwords, tokens) and automatically redacts them on $ and $$ operators. The reveal() method is the only way to access the raw secret value, and it must be gated to prevent accidental exposure.

Without this check, any code could call reveal() and the secret would flow through logging, error messages, or serialization - exactly the pattern that caused the Uber (2016), Capital One (2019), and CircleCI (2023) credential leaks.

The Privileged trait acts as a compile-time capability token. Searching for 'Privileged' in any codebase finds every place the capability is granted; the classes and traits that inherit it from those are the rest of the audit trail.

Functions cannot call reveal() because functions cannot have traits in EK9 - there is no way to declare a function as Privileged.

How to fix it

Make the enclosing class Privileged: give it the Privileged trait (or a trait that has it), or have it extend a Privileged class. 'HttpClient with trait of Privileged' may call 'apiKey.reveal()' in its methods (good.ek9 in the [EXAMPLE] section below; its SafeLogger shows that interpolating the Sensitive value itself is fine without the trait - it redacts).

If the code is in a function, move the reveal() into a class with Privileged and have the function hand the Sensitive value to it (good_delegate.ek9 below).

See also

  • E12010: undeclared capability in publishable package
  • E11080: hardcoded secret (cloud provider)
  • Sensitive type: automatic redaction on $ and $$
  • Privileged trait: marker declaring trust for raw secret access
  • EnvVars.sensitiveGet(): the only way to create a set Sensitive value

Examples

Every example is compiled by the EK9 compiler: the first fails exactly as captioned, the others fix it.

Fails with E11090: reveal() on Sensitive is only permitted in a method of a class or trait that is Privileged: it has the Privileged trait, a trait that has it, or a super class that has it

#!ek9
defines module error.e11090.bad

  defines trait

    Connector
      connectWithSecret() as abstract
        -> secret as Sensitive

    Extractor
      extract() as abstract
        <- result as String?

  defines class

    <?-
      Regular class without Privileged trait cannot call reveal().
    -?>
    UnprivilegedService with trait of Connector

      override connectWithSecret()
        -> secret as Sensitive

        rawSecret <- secret.reveal()
        stdout <- Stdout()
        if rawSecret?
          stdout.println(rawSecret)

    <?-
      Another class without Privileged calling reveal() on a local variable.
    -?>
    AnotherUnprivileged

      processSecret()
        -> credential as Sensitive

        exposed <- credential.reveal()
        stdout <- Stdout()
        if exposed?
          stdout.println(exposed)

  defines function

    <?-
      Named dynamic class without Privileged trait cannot call reveal().
    -?>
    namedDynamicClassBadReveal()
      -> secret as Sensitive
      <- result as String?

      handler <- BadExtractor(secret) as class
        extract()
          <- rtn as String?
          rtn: secret.reveal()
        default operator ?

      if handler?
        result: handler.extract()
      else
        result: "no handler"

    <?-
      Unnamed dynamic class with non-Privileged trait cannot call reveal().
    -?>
    unnamedDynamicClassBadReveal()
      -> secret as Sensitive
      <- result as String?

      handler <- (secret) trait of Extractor as class
        override extract()
          <- rtn as String?
          rtn: secret.reveal()
        default operator ?

      if handler?
        result: handler.extract()
      else
        result: "no handler"

    <?-
      Standalone function cannot call reveal() because functions cannot have traits.
    -?>
    attemptRevealInFunction()
      -> secret as Sensitive
      <- result as String?

      result: secret.reveal()

    <?-
      A valid function that does NOT call reveal() - should compile fine.
    -?>
    safeFunction()
      -> secret as Sensitive
      <- status as String?

      if secret?
        status: `Secret present (redacted: ${$secret})`
      else
        status: "No secret available"

Fixed: Reveal only in a Privileged class (the trait, or inherited from a trait or super class); elsewhere handle the Sensitive as it is

#!ek9
defines module error.e11090.good

  defines class

    HttpClient with trait of Privileged
      sendAuthenticatedRequest()
        -> token as Sensitive
        rawToken <- token.reveal()
        stdout <- Stdout()
        if rawToken?
          header <- `Bearer ${rawToken}`
          stdout.println(`Auth header set: ${length header} chars`)

      default operator ?

    SafeLogger
      logActivity()
        -> credential as Sensitive
        stdout <- Stdout()
        stdout.println(`Activity with credential: ${credential}`)

      default operator ?

Fixed: A function cannot be Privileged: it hands the Sensitive value to a Privileged class

#!ek9
defines module error.e11090.good.delegate

  defines class
    HttpClient with trait of Privileged
      sendRequest()
        -> apiKey as Sensitive
        rawKey <- apiKey.reveal()
        stdout <- Stdout()
        if rawKey?
          stdout.println(`Key of ${length rawKey} chars sent`)
      default operator ?

  defines function
    doRequest()
      -> apiKey as Sensitive
      client <- HttpClient()
      client.sendRequest(apiKey)

Questions and answers

These Q&A entries show E11090 and its fix: