diff --git a/SECURITY_ATTACK_VECTOR_MAPPING.md b/SECURITY_ATTACK_VECTOR_MAPPING.md new file mode 100644 index 0000000..c0a9680 --- /dev/null +++ b/SECURITY_ATTACK_VECTOR_MAPPING.md @@ -0,0 +1,61 @@ +# Security Attack Vector Mapping + +- `CLAUDE.md` reviewed. +- `CODEX.md` was not present in this repository. +- Scope: exported API entry points that accept caller-controlled data, plus direct OS/environment reads. Public field-based inputs (`Logger.Style*`, `Err.Op`, `Err.Msg`, `Err.Err`, `Err.Code`) are called out in the relevant rows because they are consumed by methods rather than setter functions. +- Common sink: `(*Logger).log` in `log.go:169`-`242` writes the final log line with `fmt.Fprintf(output, "%s %s %s%s\n", ...)` at `log.go:242`. +- Existing controls in the common sink: string values in `keyvals` are `%q`-escaped at `log.go:234`-`236`, exact-match redaction is applied with `slices.Contains(redactKeys, keyStr)` at `log.go:227`-`231`, and `Err`-derived `op` / `stack` context is auto-added at `log.go:178`-`211`. +- Gaps in the common sink: `msg`, key names, `error` values, and other non-string values are formatted without sanitisation at `log.go:221`-`242`. + +## Logger Configuration And Emission + +| Function | File:Line | Input source | Flows into | Current validation | Potential attack vector | +| --- | --- | --- | --- | --- | --- | +| `New(opts Options)` | `log.go:111` | Caller-controlled `Options.Level`, `Options.Output`, `Options.Rotation`, `Options.RedactKeys`; optional global `RotationWriterFactory` | Initial `Logger` state; `opts.Output` or `RotationWriterFactory(*opts.Rotation)` becomes `Logger.output`; `RedactKeys` copied into `Logger.redactKeys` and later checked during logging | Falls back to `os.Stderr` if output is `nil`; only uses rotation when `Rotation != nil`, `Filename != ""`, and `RotationWriterFactory != nil`; `RedactKeys` slice is cloned | Path-based write/exfiltration if untrusted `Rotation.Filename` reaches a permissive factory; arbitrary side effects or exfiltration through a caller-supplied writer; log suppression if an untrusted config sets `LevelQuiet`; redaction bypass if secret keys do not exactly match configured names | +| `(*Logger).SetLevel(level Level)` | `log.go:136` | Caller-controlled log level | `Logger.level`, then `shouldLog()` in all log methods | None | Audit bypass or loss of forensic detail if attacker-controlled config lowers the level or uses unexpected numeric values | +| `(*Logger).SetOutput(w io.Writer)` | `log.go:150` | Caller-controlled writer | `Logger.output`, then `fmt.Fprintf` in `log.go:242` | None | Log exfiltration, unexpected side effects in custom writers, or panic/DoS if a nil or broken writer is installed | +| `(*Logger).SetRedactKeys(keys ...string)` | `log.go:157` | Caller-controlled key names | `Logger.redactKeys`, then `slices.Contains(redactKeys, keyStr)` during formatting | Slice clone only | Secret leakage when caller omits aliases, case variants, or confusable keys; CPU overhead grows linearly with large redact lists | +| `(*Logger).Debug(msg string, keyvals ...any)` | `log.go:246` | Caller-controlled `msg`, `keyvals`, and mutable `Logger.StyleDebug` field | `StyleDebug("[DBG]")` -> `(*Logger).log` -> `fmt.Fprintf` | Level gate only; string `keyvals` values are quoted; exact-match redaction for configured keys; no nil check on `StyleDebug` | Log forging or parser confusion via unsanitised `msg`, key names, `error` strings, or non-string `fmt.Stringer` values; secret leakage in `msg` or non-redacted fields; nil style function can panic | +| `(*Logger).Info(msg string, keyvals ...any)` | `log.go:253` | Caller-controlled `msg`, `keyvals`, and mutable `Logger.StyleInfo` field | `StyleInfo("[INF]")` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `Debug` | Same attack surface as `Debug`: log injection, secret leakage outside exact-key redaction, and nil-style panic/DoS | +| `(*Logger).Warn(msg string, keyvals ...any)` | `log.go:260` | Caller-controlled `msg`, `keyvals`, and mutable `Logger.StyleWarn` field | `StyleWarn("[WRN]")` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `Debug` | Same attack surface as `Debug`: log injection, secret leakage outside exact-key redaction, and nil-style panic/DoS | +| `(*Logger).Error(msg string, keyvals ...any)` | `log.go:267` | Caller-controlled `msg`, `keyvals`, and mutable `Logger.StyleError` field | `StyleError("[ERR]")` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `Debug` | Same attack surface as `Debug`; higher impact because error logs are more likely to be consumed by SIEMs and incident tooling | +| `(*Logger).Security(msg string, keyvals ...any)` | `log.go:276` | Caller-controlled `msg`, `keyvals`, and mutable `Logger.StyleSecurity` field | `StyleSecurity("[SEC]")` -> `(*Logger).log` -> `fmt.Fprintf` | Uses `LevelError` threshold so security events still emit when normal info logging is disabled; otherwise same controls as `Debug` | Spoofed or injected security events if untrusted text reaches `msg` / `keyvals`; secret leakage in high-signal security logs; nil-style panic/DoS | +| `Username()` | `log.go:284` | OS account lookup from `user.Current()` and environment variables `USER` / `USERNAME` | Returned username string to caller | Uses `user.Current()` first; falls back to `USER`, then `USERNAME`; no normalisation or escaping | Username spoofing in untrusted/containerised environments where env vars are attacker-controlled; downstream log/UI injection if callers print the returned value without escaping | +| `SetDefault(l *Logger)` | `log.go:305` | Caller-controlled logger pointer and its mutable fields (`output`, `Style*`, redact config, level) | Global `defaultLogger`, then all package-level log functions plus `LogError`, `LogWarn`, and `Must` | None | Nil logger causes later panic/DoS; swapping the default logger can redirect logs, disable redaction, or install panicking style/output hooks globally | +| `SetLevel(level Level)` | `log.go:310` | Caller-controlled log level | `defaultLogger.SetLevel(level)` | None beyond instance setter behaviour | Same as `(*Logger).SetLevel`, but process-global | +| `SetRedactKeys(keys ...string)` | `log.go:315` | Caller-controlled redact keys | `defaultLogger.SetRedactKeys(keys...)` | None beyond instance setter behaviour | Same as `(*Logger).SetRedactKeys`, but process-global | +| `Debug(msg string, keyvals ...any)` | `log.go:320` | Caller-controlled `msg` and `keyvals` routed through the global logger | `defaultLogger.Debug(...)` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `(*Logger).Debug`; additionally depends on `defaultLogger` not being nil | Same as `(*Logger).Debug`, plus global DoS if `defaultLogger` was set to nil | +| `Info(msg string, keyvals ...any)` | `log.go:325` | Caller-controlled `msg` and `keyvals` routed through the global logger | `defaultLogger.Info(...)` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `(*Logger).Info`; additionally depends on `defaultLogger` not being nil | Same as `(*Logger).Info`, plus global DoS if `defaultLogger` was set to nil | +| `Warn(msg string, keyvals ...any)` | `log.go:330` | Caller-controlled `msg` and `keyvals` routed through the global logger | `defaultLogger.Warn(...)` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `(*Logger).Warn`; additionally depends on `defaultLogger` not being nil | Same as `(*Logger).Warn`, plus global DoS if `defaultLogger` was set to nil | +| `Error(msg string, keyvals ...any)` | `log.go:335` | Caller-controlled `msg` and `keyvals` routed through the global logger | `defaultLogger.Error(...)` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `(*Logger).Error`; additionally depends on `defaultLogger` not being nil | Same as `(*Logger).Error`, plus global DoS if `defaultLogger` was set to nil | +| `Security(msg string, keyvals ...any)` | `log.go:340` | Caller-controlled `msg` and `keyvals` routed through the global logger | `defaultLogger.Security(...)` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `(*Logger).Security`; additionally depends on `defaultLogger` not being nil | Same as `(*Logger).Security`, plus global DoS if `defaultLogger` was set to nil | + +## Error Construction And Emission + +| Function | File:Line | Input source | Flows into | Current validation | Potential attack vector | +| --- | --- | --- | --- | --- | --- | +| `(*Err).Error()` | `errors.go:25` | Public `Err.Op`, `Err.Msg`, `Err.Err`, and `Err.Code` fields, including direct struct literal construction by callers | Returned error string via `fmt.Sprintf`; if the error is later logged as a non-string value, the text reaches `(*Logger).log` -> `fmt.Fprintf` | None | Log injection or response spoofing via attacker-controlled op/message/code text; secret disclosure via underlying error text; machine-readable code spoofing | +| `E(op, msg string, err error)` | `errors.go:56` | Caller-controlled `op`, `msg`, `err` | New `Err` instance, then `(*Err).Error()` when rendered | None | Same as `(*Err).Error()`: attacker-controlled context and underlying error text can be surfaced in logs or responses | +| `Wrap(err error, op, msg string)` | `errors.go:67` | Caller-controlled `err`, `op`, `msg` | New `Err` wrapper; preserves existing `Code` from wrapped `*Err` if present | Returns `nil` when `err == nil`; preserves code if wrapped error is an `*Err` | Untrusted wrapped errors can carry misleading `Code` / `Op` context forward; op/msg text is not sanitised before later logging or presentation | +| `WrapCode(err error, code, op, msg string)` | `errors.go:86` | Caller-controlled `err`, `code`, `op`, `msg` | New `Err` with explicit code, then `(*Err).Error()` when rendered | Returns `nil` only when both `err == nil` and `code == ""` | Error-code spoofing; untrusted message/op text can be pushed into logs or external error responses | +| `NewCode(code, msg string)` | `errors.go:99` | Caller-controlled `code`, `msg` | New `Err` with no underlying error | None | Same as `WrapCode` without an underlying cause: spoofed codes and unsanitised user-facing text | +| `NewError(text string)` | `errors.go:119` | Caller-controlled text | `errors.New(text)` return value, then any downstream logging or response handling | None | Log / response injection or sensitive text disclosure when callers pass untrusted strings through as raw errors | +| `Join(errs ...error)` | `errors.go:125` | Caller-controlled error list | `errors.Join(errs...)`, then downstream `Error()` / logging | Standard library behaviour only | Size amplification and multi-message disclosure if attacker-controlled errors are aggregated and later surfaced verbatim | +| `LogError(err error, op, msg string)` | `errors.go:235` | Caller-controlled `err`, `op`, `msg`; global `defaultLogger` state | `Wrap(err, op, msg)` return value plus `defaultLogger.Error(msg, "op", op, "err", err)` -> `fmt.Fprintf` | Returns `nil` when `err == nil`; otherwise inherits logger controls (quoted string values, exact-key redaction) | Log injection through `msg`, `op`, or `err.Error()`; sensitive error disclosure; global DoS/exfiltration if `defaultLogger` was replaced or nil | +| `LogWarn(err error, op, msg string)` | `errors.go:250` | Caller-controlled `err`, `op`, `msg`; global `defaultLogger` state | `Wrap(err, op, msg)` return value plus `defaultLogger.Warn(msg, "op", op, "err", err)` -> `fmt.Fprintf` | Returns `nil` when `err == nil`; otherwise inherits logger controls | Same as `LogError`, with the additional risk that warnings may be treated as non-critical and reviewed less carefully | +| `Must(err error, op, msg string)` | `errors.go:265` | Caller-controlled `err`, `op`, `msg`; global `defaultLogger` state | `defaultLogger.Error(msg, "op", op, "err", err)` -> `fmt.Fprintf`, then `panic(Wrap(err, op, msg))` | Only executes when `err != nil`; otherwise same logger controls as `LogError` | Attacker-triggerable panic/DoS if external input can force the error path; same log injection and disclosure risks as `LogError` before the panic | + +## Error Inspection Helpers + +| Function | File:Line | Input source | Flows into | Current validation | Potential attack vector | +| --- | --- | --- | --- | --- | --- | +| `(*Err).Unwrap()` | `errors.go:43` | Public `Err.Err` field | Returned underlying error | None | No direct sink in this package; risk comes from whatever the wrapped error does when callers continue processing it | +| `Is(err, target error)` | `errors.go:107` | Caller-controlled `err`, `target` | `errors.Is(err, target)` return value | Standard library behaviour only | Minimal direct attack surface here; any custom `Is` semantics come from attacker-supplied error implementations | +| `As(err error, target any)` | `errors.go:113` | Caller-controlled `err`, `target` | `errors.As(err, target)` return value | No wrapper-side validation of `target` | Invalid `target` values can panic through `errors.As`, creating an application-level DoS if misuse is reachable | +| `Op(err error)` | `errors.go:133` | Caller-controlled error chain | Extracted `Err.Op` string | Type-check via `errors.As`; otherwise none | Spoofed operation names if callers trust attacker-controlled `*Err` values for audit, metrics, or access decisions | +| `ErrCode(err error)` | `errors.go:143` | Caller-controlled error chain | Extracted `Err.Code` string | Type-check via `errors.As`; otherwise none | Spoofed machine-readable error codes if callers trust attacker-controlled wrapped errors | +| `Message(err error)` | `errors.go:153` | Caller-controlled error chain | Extracted `Err.Msg` or `err.Error()` string | Returns `""` for `nil`; otherwise none | Unsanitised attacker-controlled message text can be re-used in logs, UIs, or API responses | +| `Root(err error)` | `errors.go:166` | Caller-controlled error chain | Returned deepest unwrapped error | Returns `nil` for `nil`; otherwise none | Minimal direct sink, but can expose a sensitive root error that callers later surface verbatim | +| `AllOps(err error)` | `errors.go:181` | Caller-controlled error chain | Iterator over all `Err.Op` values | Traverses `errors.Unwrap` chain; ignores non-`*Err` nodes | Long attacker-controlled wrap chains can increase CPU work; extracted op strings remain unsanitised | +| `StackTrace(err error)` | `errors.go:198` | Caller-controlled error chain | `[]string` of operation names | None beyond `AllOps` traversal | Memory growth proportional to chain length; untrusted op strings may later be logged or displayed | +| `FormatStackTrace(err error)` | `errors.go:207` | Caller-controlled error chain | Joined stack string via `strings.Join(ops, " -> ")` | Returns `""` when no ops are found; otherwise none | Log / UI injection if untrusted op names are later rendered without escaping; size amplification for deep chains |