Why can't I mark a captured variable as 'sanitized' in a dynamic function?

← Security and Sanitization · Ref: Q1332

The 'sanitized' modifier cannot be applied to a captured variable in a dynamic function or dynamic class (E07941). By the time a value is captured into a closure it has already crossed the trust boundary, so re-asserting sanitization there is both meaningless and misleading.

Sanitization is a property of the ENTRY POINT - the function or method parameter where untrusted external data first enters the system. Mark that parameter 'sanitized' and the compiler injects a defensive sanitizing copy. The dynamic function then captures the already-clean value with a plain capture (no modifier).

WRONG

  build()
    -> raw as String
    fn <- (sanitized raw) is Formatter as pure function ...   // E07941

RIGHT

  build()
    -> raw as sanitized String          // sanitize at the boundary
    fn <- (raw) is Formatter as pure function ...   // capture the clean value

See Q950 for the full set of places 'sanitized' is restricted (captures E07941, declarations E07943).

Example

defines module qa.sanitizeddeep.captureboundary

  defines function

    <?-
      Abstract function contract the dynamic function will implement.
    -?>
    Formatter as pure abstract
      -> text as String
      <- output as String?

    <?-
      THE FIX: sanitize at the ENTRY POINT (the parameter), not at the capture.
      The compiler injects a sanitizing defensive copy for 'rawInput' here.
      The dynamic function then captures the already-clean value with a plain
      capture list (no 'sanitized' modifier - that would be E07941).
    -?>
    buildPrefixer() as pure
      -> rawInput as sanitized String
      <- made as Formatter?

      made: (prefix: rawInput) is Formatter as pure function
        output: `${prefix}: ${text}`

  defines program

    SanitizedCaptureDemo()
      stdout <- Stdout()

      formatter <- buildPrefixer("user-data")
      stdout.println(formatter("hello"))
      stdout.println("sanitize at the parameter, capture the clean value")

Common mistakes

E07941 — 'sanitized' cannot be applied to a captured variable - it belongs on the entry-point parameter where untrusted data enters. See ek9 -h E07941 for details.

Incorrect:

made: (prefix: sanitized rawInput) is Formatter as pure function

Correct:

made: (prefix: rawInput) is Formatter as pure function
Other ways to ask this
  • What triggers E07941 SANITIZED_NOT_ON_CAPTURED?
  • How do I sanitize a value before capturing it into a closure?
  • Why is 'sanitized' rejected on a dynamic function or class capture list?

Coming from another language?

Java/Kotlin/Python: no language-level taint tracking - a closure can capture tainted data and developers must remember to sanitize manually somewhere, with no compiler check. EK9 forces sanitization to live at the single entry-point parameter and rejects it on captures at compile time (E07941), so the trust boundary stays in exactly one obvious place.

Keywords: trust, captured, boundary, security, closure, dynamic, sanitized, function, capture, E07941