Create me an AuditLogger class whose constructor takes a sanitized String log message, where construction with malicious input (file path traversal, terminal escape sequences) leaves the logger in a 'rejected' state.

← Security and Sanitization · Ref: Q1246

The 'sanitized' modifier works on constructor parameters identically to function and method parameters. When a class is constructed with malicious input, the constructor sees an UNSET parameter, allowing the object to set itself into a safe default state rather than throwing an exception.

CLASS WITH SANITIZED CONSTRUCTOR PARAMETER

  AuditLogger
    message as String: "<no message>"
    state as String: "empty"
    AuditLogger()
      -> incomingMessage as sanitized String
      if incomingMessage?
        message: incomingMessage
        state: "clean"
      else
        state: "rejected"
    getMessage()
      <- rtn as String: message
    getState()
      <- rtn as String: state
    default operator ?

The field defaults ensure the object is always well-formed. The constructor body branches on 'incomingMessage?' to record whether the input was accepted.

DEMONSTRATING ACCEPTED AND REJECTED INPUTS

This example varies the payload types — file path traversal, ANSI terminal escape codes (which the runtime reports as LOG_INJECTION), and a bare embedded newline — rather than the usual SQL/XSS strings, to show that 'sanitized' applies to ANY untrusted input, not just web payloads. It also shows what is NOT rejected: a plain newline inside the text is not treated as a threat, so the logger reports 'clean' for it:

  // Clean log message
  log1 <- AuditLogger("User alice logged in successfully")
  // "clean"
  // Path traversal attempt
  log2 <- AuditLogger("file=../../../etc/shadow")
  // "rejected"
  // ANSI escape attempt (reported as LOG_INJECTION)
  log3 <- AuditLogger("Login\u001b[31m FAILED \u001b[0m")
  // "rejected"
  // A bare embedded newline is NOT flagged
  log4 <- AuditLogger("action=lookup

2025-01-01 ADMIN_OVERRIDE")

  // "clean"

Each rejection also writes a JSON warning line to stderr naming the threat (PATH_TRAVERSAL, LOG_INJECTION). If your log format is line-oriented, split or escape newlines yourself: 'sanitized' does not do it for you.

KEY ADVANTAGE

The object remains usable even when the input was malicious — it just records 'rejected' instead of 'clean'. The caller can inspect getState() to know the outcome. No exceptions, no try/catch, no nullable result wrapping.

See Q1237 for sanitized function parameters. See Q1247 for sanitized method parameters. See Q272 for defense in depth.

Example

#!ek9
defines module qa.security.auditloggerconstructor

  defines class

    AuditLogger
      message as String: "<no message>"
      state as String: "empty"

      AuditLogger()
        -> incomingMessage as sanitized String

        if incomingMessage?
          message: incomingMessage
          state: "clean"
        else
          state: "rejected"

      getMessage()
        <- rtn as String: message

      getState()
        <- rtn as String: state

      default operator ?

  defines program

    SanitizedAuditLoggerDemo()
      stdout <- Stdout()

      stdout.println("=== AuditLogger Sanitization Demo ===")

      cleanMessage <- "User alice logged in successfully"
      log1 <- AuditLogger(cleanMessage)
      stdout.println("Clean: " + log1.getState())

      pathTraversal <- "file=../../../etc/shadow"
      log2 <- AuditLogger(pathTraversal)
      stdout.println("Path: " + log2.getState())

      ansiEscape <- "Login\u001b[31m FAILED \u001b[0m"
      log3 <- AuditLogger(ansiEscape)
      stdout.println("ANSI: " + log3.getState())

      logInjection <- "action=lookup\n2025-01-01 ADMIN_OVERRIDE"
      log4 <- AuditLogger(logInjection)
      stdout.println("Inject: " + log4.getState())

      stdout.println("=== Complete ===")

Common mistakes

E07910 — The sanitized modifier can only be applied to String parameters. Injection attacks travel as text, so a non-String type has nothing to clean. See ek9 -h E07910 for details.

Incorrect:

-> incomingMessage as sanitized Integer

Correct:

-> incomingMessage as sanitized String
Other ways to ask this
  • Show me how the sanitized keyword works on a constructor parameter for an audit logger.
  • Write a logging class that rejects malicious log messages at construction time.
  • Implement an AuditLogger that validates its incoming message via the sanitized modifier.
  • Build an object that records whether its construction message was clean or rejected.

Coming from another language?

Java: validation in setters with throw on invalid input, or Optional<AuditLogger> wrapper. Kotlin: data class with init { require(...) } that throws. Rust: TryFrom<String> for AuditLogger with Result return type. Python: __init__ raises ValueError. EK9: 'sanitized' modifier on the constructor parameter — input is unset if rejected, object always constructs successfully with a state field indicating which path was taken.

Keywords: ANSI escape, AuditLogger, audit, constructor, input validation, log injection, path traversal, sanitized