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