Question

I have a security tool that sends users their new password through email. The production email module (that I don’t own and don’t want to change) will log the entire html email message body using Log4Net when the threshold is VERBOSE. Since the email contains a domain user’s password in clear text, I would like to remove the password from the log messages before it reaches the appenders.

Is there a way for me to temporary insert an object into the Log4Net stack that would allow me to search the LoggingEvent message and alter it to mask out any passwords that I find? I’d like to insert the object, call the email module, and then remove the object.

Was it helpful?

Solution

I would probably write a pattern converter. You can find an example here. Your implementation could be like this:

protected override void Convert(TextWriter writer, LoggingEvent loggingEvent)
{
    string msg = loggingEvent.RenderedMessage;
    // remove the password if there is any
    writer.Write(msg);
}

OTHER TIPS

I had a similar problem, and I solved it by inheriting from ForwardingAppender and then modifying the LoggingEvent (using reflection) before passing it on.

using System.Reflection;
using log4net.Appender;
using log4net.Core;

class MessageModifyingForwardingAppender : ForwardingAppender
{
    private static FieldInfo _loggingEventm_dataFieldInfo;

    public MessageModifyingForwardingAppender()
    {
        _loggingEventm_dataFieldInfo = typeof(LoggingEvent).GetField("m_data", BindingFlags.Instance | BindingFlags.NonPublic);
    }

    protected override void Append(LoggingEvent loggingEvent)
    {
        var originalRenderedMessage = loggingEvent.RenderedMessage;

        var newMessage = GetModifiedMessage(originalRenderedMessage);

        if (originalRenderedMessage != newMessage)
            SetMessageOnLoggingEvent(loggingEvent, newMessage);

        base.Append(loggingEvent);
    }

    /// <summary>
    /// I couldn't figure out how to 'naturally' change the log message, so use reflection to change the underlying storage of the message data
    /// </summary>
    private static void SetMessageOnLoggingEvent(LoggingEvent loggingEvent, string newMessage)
    {
        var loggingEventData = (LoggingEventData)_loggingEventm_dataFieldInfo.GetValue(loggingEvent);
        loggingEventData.Message = newMessage;
        _loggingEventm_dataFieldInfo.SetValue(loggingEvent, loggingEventData);
    }

    private static string GetModifiedMessage(string originalMessage)
    {
        // TODO modification implementation
        return originalMessage;
    }
}

It's not very pretty, but it works.

Then you need a log4net config that looks something like this

<log4net>
    <appender name="ModifyingAppender" type="Your.Lib.Log4Net.MessageModifyingForwardingAppender,Your.Lib">
        <appender-ref ref="ConsoleAppender" />
    </appender>
    <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="%date %-5level [%thread] %logger: %message%newline"/>
        </layout>
    </appender>
    <root>
        <level value="INFO"/>
        <appender-ref ref="ModifyingAppender"/>
    </root>
</log4net>

and an implementation of GetModifiedMessage() that suits your need, and you are away!

A small improvement to Chris Priest's solution: inherit your appender not from ForwardingAppender, but from base class AppenderSkeleton. It makes configuration a bit simpler - you don't need to reference other appenders from your one and apply it easily to different loggers now

public class PasswordObfuscationAppender : AppenderSkeleton
{
    private static readonly FieldInfo LoggingEventmDataFieldInfo = typeof(LoggingEvent).GetField(
        "m_data",
        BindingFlags.Instance | BindingFlags.NonPublic);

    protected override void Append(LoggingEvent loggingEvent)
    {
        var originalRenderedMessage = loggingEvent.RenderedMessage;

        var newMessage = GetModifiedMessage(originalRenderedMessage);

        if (originalRenderedMessage != newMessage)
            SetMessageOnLoggingEvent(loggingEvent, newMessage);
    }

    /// <summary>
    /// I couldn't figure out how to 'naturally' change the log message, so use reflection to change the underlying storage of the message data
    /// </summary>
    private static void SetMessageOnLoggingEvent(LoggingEvent loggingEvent, string newMessage)
    {
        var loggingEventData = (LoggingEventData)LoggingEventmDataFieldInfo.GetValue(loggingEvent);
        loggingEventData.Message = newMessage;
        LoggingEventmDataFieldInfo.SetValue(loggingEvent, loggingEventData);
    }

    private static string GetModifiedMessage(string originalMessage)
    {
        // TODO modification implementation
        return originalMessage;
    }
}

usage

<appender name="PasswordObfuscationAppender" type="Foundation.PasswordObfuscationAppender,Foundation" />

<appender name="MainAppender" type="log4net.Appender.RollingFileAppender">
  <file value="..\Logs\File.log" />
</appender>

<root>
  <level value="DEBUG" />
  <appender-ref ref="PasswordObfuscationAppender" />
  <appender-ref ref="MainAppender" />
</root>

Another kind of solution is to intercept the LoggingEvent before reaching any appender directly from the Logger. One prerequisite is to be able to modify the Root Hierarchy before creating any Logger.

In the sample below, we just recreate a new LoggingEvent, but it's not necessary if you care about intensive memory copy, with reflexion you can access the underlying LoggingEventData(is struct) and set new values to the fields directly.

You just need to call InterceptLoggerFactory.Apply() before any LogManager.GetLogger().

public class InterceptLoggerFactory : ILoggerFactory
{
    public static void Apply() => Apply((Hierarchy)LogManager.GetRepository());
    public static void Apply(Hierarchy h) => h.LoggerFactory = new InterceptLoggerFactory();

    public Logger CreateLogger(ILoggerRepository repository, string name)
    {
        if (name == null) return new InterceptRootLogger(repository.LevelMap.LookupWithDefault(Level.Debug));
        return new InterceptLogger(name);
    }


    class InterceptLogger : Logger
    {
        public InterceptLogger(string name) : base(name)
        {
        }

        protected override void CallAppenders(LoggingEvent loggingEvent)
        {
            // Implement interception of property on loggingEvent before any call to any appender (execution is sync).
            /*
             * var loggingEventData = loggingEvent.GetLoggingEventData();
             * loggingEventData.Message = [EncryptMessage](loggingEventData.Message);
             * var newLoggingEvent = new LoggingEvent(loggingEventData);
             * base.CallAppenders(newLoggingEvent);
             * */
            base.CallAppenders(loggingEvent);
        }
    }

    class InterceptRootLogger : RootLogger
    {
        public InterceptRootLogger(Level level) : base(level)
        {
        }

        protected override void CallAppenders(LoggingEvent loggingEvent)
        {
            // Implement interception of property on loggingEvent before any call to any appender (execution is sync).
            base.CallAppenders(loggingEvent);
        }
    }
}

This improves upon @jeremy-fizames's, where you don't need to worry about whether the ILoggerFactory is set before any LogManager.GetLogger() is called. You can use log4net's plugin framework to ensure this InterceptLoggerFactory is set before any root logger is assigned. The [assembly:] attribute ensures that log4net finds and loads the plugin before it sets up any logging.

This solution works without modifying how existing appenders are configured, and it works whether you're loading log4net config from XML and/or programmatically at runtime.

// Register intercept as a log4net plugin
[assembly: log4net.Config.Plugin(typeof(InterceptPlugin))]

public class InterceptPlugin : log4net.Plugin.PluginSkeleton
{
    public InterceptPlugin() : base("Intercept") {}

    public override void Attach(ILoggerRepository repository)
    {
        base.Attach(repository);

        ((Hierarchy)repository).LoggerFactory = new InterceptLoggerFactory();
    }
}

// @jeremy-fizames's ILoggerFactory
public class InterceptLoggerFactory : ILoggerFactory
{
    public Logger CreateLogger(ILoggerRepository repository, string name)
    {
        if (name == null) return new InterceptRootLogger(repository.LevelMap.LookupWithDefault(Level.Debug));
        return new InterceptLogger(name);
    }

    class InterceptLogger : Logger
    {
        public InterceptLogger(string name) : base(name) {}

        protected override void CallAppenders(LoggingEvent loggingEvent)
        {
            // Implement interception of property on loggingEvent before any call to any appender (execution is sync).
            base.CallAppenders(loggingEvent);
        }
    }

    class InterceptRootLogger : RootLogger
    {
        public InterceptRootLogger(Level level) : base(level) {}

        protected override void CallAppenders(LoggingEvent loggingEvent)
        {
            // Implement interception of property on loggingEvent before any call to any appender (execution is sync).
            base.CallAppenders(loggingEvent);
        }
    }
}

You can try intercepting the calls to log4net using the Unity Application Block method interceptor. Or you could write a custom log4net appender.

log4net is open source, you can modify it.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top