Editing Log4Net messages before they reach the app

2019-01-28 09:41发布

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.

7条回答
倾城 Initia
2楼-- · 2019-01-28 09:47

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

查看更多
霸刀☆藐视天下
3楼-- · 2019-01-28 09:55

log4net is open source, you can modify it.

查看更多
beautiful°
4楼-- · 2019-01-28 09:56

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);
        }
    }
}
查看更多
叛逆
5楼-- · 2019-01-28 10:01

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);
}
查看更多
Bombasti
6楼-- · 2019-01-28 10:07

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!

查看更多
混吃等死
7楼-- · 2019-01-28 10:09

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);
        }
    }
}
查看更多
登录 后发表回答