Can an override add or drop 'sanitized' on a parameter?

← Security and Sanitization · Ref: Q663

'sanitized' is part of the contract of a method or function, like 'pure'. An override may ADD it to a parameter, but may never DROP it (E07940). This holds for class methods, trait methods and functions that are 'is' a super function.

WHY DROPPING IS REJECTED

Consider polymorphic dispatch:

  handler <- getHandler()      // Could be Base or Derived
  handler.process(userInput)   // Base promised to clean userInput

If Derived dropped 'sanitized', untrusted input would reach code that never cleans it whenever the runtime type is Derived.

WHY ADDING IS ALLOWED

The receiving method or function cleans its own 'sanitized' parameters, so an override that adds 'sanitized' cleans its input on every route - including a call through the base type. A subtype may weaken a precondition, never strengthen it (Liskov Substitution Principle).

CORRECT PATTERNS

  defines class Base as open
    process()
      -> input as sanitized String
  defines class Derived extends Base
    override process()
      -> input as sanitized String    // must keep it
  plainFormatter() as abstract
    -> text as String
  strictFormatter() is plainFormatter
    -> text as sanitized String       // may add it

EVERY PARENT COUNTS

A method that implements a trait AND overrides a super (or implements two traits) must keep 'sanitized' if ANY of them declares it.

DISPATCHERS

Every dispatcher handler must keep the 'sanitized' parameters of its dispatcher entry point: a call made through the entry point is routed to the handler before the entry point's own body runs, so the handler must clean the value itself.

TRAIT NOTE

Traits cannot have 'default operator ?' (E07030). When defining a trait with sanitized methods, only classes implementing the trait can have 'default operator ?'.

See Q661 for sanitized basics. See Q662 for safe copy patterns. See Q664 for increment expression restriction. See Q215 for sanitized overview.

Example

defines module qa.sanitizeddeep.overridematch

  defines function

    <?-
      Abstract function whose parameter is sanitized.
      Every implementation must keep it sanitized.
    -?>
    cleanFormatter() as abstract
      -> text as sanitized String
      <- result as String?

    <?-
      Keeps the sanitized parameter of its super function.
    -?>
    bracketed() is cleanFormatter
      -> incoming as sanitized String
      <- result as String: ""

      safeCopy <- String(incoming)
      result: `[${safeCopy}]`

  defines trait

    <?-
      Trait defining sanitized contract.
      All implementations must preserve sanitized on the parameter.
    -?>
    InputProcessor
      process() as abstract
        -> inputText as sanitized String
        <- result as String?

  defines class

    <?-
      Base class with one sanitized parameter and one plain parameter.
      Subclasses MUST keep sanitized where the base has it, and MAY add it.
    -?>
    BaseHandler as open
      handleRequest()
        -> requestBody as sanitized String
        <- response as String: ""

        safeCopy <- String(requestBody)
        response: safeCopy

      describe()
        -> label as String
        <- summary as String: ""

        summary: `Base describes: ${label}`

      default operator ?

    <?-
      Keeps sanitized on handleRequest, and ADDS it on describe.
      Security behavior is at least as strict as the parent's.
    -?>
    DerivedHandler extends BaseHandler
      override handleRequest()
        -> requestBody as sanitized String
        <- response as String: ""

        safeCopy <- String(requestBody)
        response: "Derived: " + safeCopy

      override describe()
        -> label as sanitized String
        <- summary as String: ""

        safeCopy <- String(label)
        summary: `Derived describes: ${safeCopy}`

      default operator ?

    <?-
      Trait implementation with matching sanitized.
    -?>
    SafeProcessor with trait of InputProcessor
      override process()
        -> inputText as sanitized String
        <- result as String: ""

        localCopy <- String(inputText)
        result: "Processed: " + localCopy

      default operator ?

  defines program

    OverrideMatchDemo()
      stdout <- Stdout()

      baseHandler <- BaseHandler()
      baseResponse <- baseHandler.handleRequest("base input")
      stdout.println(baseResponse)

      derivedHandler <- DerivedHandler()
      derivedResponse <- derivedHandler.handleRequest("derived input")
      stdout.println(derivedResponse)

      processor <- SafeProcessor()
      processResult <- processor.process("trait input")
      stdout.println(processResult)

      asBase as BaseHandler: derivedHandler
      stdout.println(asBase.describe("through the base"))

      stdout.println(bracketed("formatted"))

Common mistakes

E50060 — String has no toUpperCase() method in EK9. Use upperCase() instead. See ek9 -h E50060 for details.

Incorrect:

baseResponse <- baseHandler.handleRequest("base input").toUpperCase()

Correct:

baseResponse <- baseHandler.handleRequest("base input")

E07940 — The super function cleanFormatter() marks its parameter 'sanitized', so an implementation may not drop it - a caller holding cleanFormatter expects the input to be cleaned. See ek9 -h E07940 for details.

Incorrect:

-> incoming as String

Correct:

-> incoming as sanitized String
Other ways to ask this
  • What is E07940 sanitized dropped in an override?
  • Can I add sanitized to an override method?
  • What happens if I remove sanitized from an overriding method?

Coming from another language?

Java: no enforcement, override can change validation. Python: no enforcement. Go: no inheritance. Rust: trait implementations must match exactly. EK9: the compiler rejects an override that drops sanitized (E07940) and allows one that adds it, for methods and functions alike.

Keywords: Liskov, LSP, abstract, visitor, virtual, E07940, sanitized, function, validate, handler, match, polymorphic, override, sealed, sanitize, security, open