Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,32 @@

package org.opensearch.security.auditlog.sink;

import java.util.regex.Pattern;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;

import org.opensearch.common.settings.Settings;
import org.opensearch.security.auditlog.impl.AuditMessage;

public final class Log4JSink extends AuditLogSink {

// MDC key constants — single source of truth for put/remove consistency
static final String MDC_CATEGORY = "audit_category";
static final String MDC_ACTION = "audit_action";
static final String MDC_USER = "audit_user";
static final String MDC_REQUEST_TYPE = "audit_request_type";

/** Characters unsafe for filenames + control chars — pre-compiled for performance. */
private static final Pattern UNSAFE_MDC_CHARS = Pattern.compile("[:/\\\\*?\"<>|\\[\\]\\p{Cntrl}]");

final Logger auditLogger;
final String loggerName;
final Level logLevel;
final boolean enabled;
final boolean mdcRoutingEnabled;
final Integer maximumIndexCharactersPerMessage;

public Log4JSink(final String name, final Settings settings, final String settingsPrefix, AuditLogSink fallbackSink) {
Expand All @@ -36,6 +49,7 @@ public Log4JSink(final String name, final Settings settings, final String settin
Integer.MAX_VALUE
);
enabled = auditLogger.isEnabled(logLevel);
mdcRoutingEnabled = settings.getAsBoolean(settingsPrefix + ".log4j.enable_mdc_routing", false);
}

public boolean isHandlingBackpressure() {
Expand All @@ -44,8 +58,43 @@ public boolean isHandlingBackpressure() {

public boolean doStore(final AuditMessage msg) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional — unconditional per-event cost. This runs 4x put + 4x regex + 4x remove on every audit event regardless of whether any operator has configured a RoutingAppender. On high-volume clusters that's overhead nobody using the default config benefits from. Consider gating enrichment behind a setting (e.g. ...log4j.enable_mdc_routing, default off) so those clusters pay nothing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

if (enabled) {
msg.toJsonSplitIndices(maximumIndexCharactersPerMessage).forEach(message -> auditLogger.log(logLevel, message));
if (mdcRoutingEnabled) {
try {
// Push audit attributes into Log4j MDC so operators can use
// RoutingAppender with $${ctx:audit_category} etc. to split logs.
// WARNING: Do not use audit_user as a RoutingAppender routing key for file paths —
// it is unbounded/attacker-influenceable and could cause inode/disk exhaustion.
// Prefer audit_category (bounded enum) for file-based routing.
ThreadContext.put(MDC_CATEGORY, sanitizeForMdc(msg.getCategory() != null ? msg.getCategory().name() : null));
ThreadContext.put(MDC_ACTION, sanitizeForMdc(msg.getPrivilege()));
ThreadContext.put(MDC_USER, sanitizeForMdc(msg.getEffectiveUser()));
ThreadContext.put(MDC_REQUEST_TYPE, sanitizeForMdc(msg.getRequestType()));

msg.toJsonSplitIndices(maximumIndexCharactersPerMessage).forEach(message -> auditLogger.log(logLevel, message));
} finally {
ThreadContext.remove(MDC_CATEGORY);
ThreadContext.remove(MDC_ACTION);
ThreadContext.remove(MDC_USER);
ThreadContext.remove(MDC_REQUEST_TYPE);
}
} else {
msg.toJsonSplitIndices(maximumIndexCharactersPerMessage).forEach(message -> auditLogger.log(logLevel, message));
}
}
return true;
}

/**
* Sanitizes a value for safe use in Log4j MDC, particularly when operators
* use MDC values in RoutingAppender file paths. Replaces characters that are
* problematic in filenames (e.g., colons in action names like "indices:data/write/index")
* and control characters that could enable log-forging attacks.
* These MDC keys are owned by the audit sink — no other code should set them.
*/
static String sanitizeForMdc(String value) {
if (value == null || value.isEmpty()) {
return "unknown";
}
return UNSAFE_MDC_CHARS.matcher(value).replaceAll("_");
}
}
Loading
Loading