如何使用log4net在较长的字符串中屏蔽或隐藏序列化JSON对象中的敏感值

yeotifhr  于 2023-03-13  发布在  其他
关注(0)|答案(1)|浏览(156)

作为任务的一部分,我必须在日志记录期间屏蔽包含密码等的属性的敏感值,并以不更改调用代码的方式进行屏蔽。
例如,在ASP.NET核心项目的客户端中,调用代码如下所示:

ILog logger = LogManager.GetLogger(this.GetType());
RequestDto dto = new RequestDto();
dto.Body = new RequestDtoBody();
dto.Body.pwd = "12345";
// Fill all the necessary properties to make the request...

logger.Debug("Making request: " + JsonConvert.SerializeObject(dto)); //...

属性pwd的值应替换为***,但记录器仅接收字符串消息。

0x6upsns

0x6upsns1#

经过大量的研究和实验,我能够通过扩展LayoutSkeleton并将消息修改为如下形式来提出解决方案:

using log4net.Core;
using log4net.Layout;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace Company.Project.Logging
{
    public class MaskLayout : LayoutSkeleton
    {
        private string _search;

        public string Search
        {
            get { return _search; }
            set { _search = value; }
        }

        private string _mask;

        public string Mask
        {
            get { return _mask; }
            set { _mask = value; }
        }

        public MaskLayout()
        {
            IgnoresException = true;
        }

        public override void ActivateOptions()
        {
        }

        public override void Format(TextWriter writer, LoggingEvent loggingEvent)
        {
            if (loggingEvent == null)
            {
                throw new ArgumentNullException("loggingEvent");
            }

            // We receive a string which potentially has JSON object(s) inside that need to be checked.
            var message = loggingEvent.RenderedMessage;
            if (message == null)
            {
                return;
            }

            var search = _search.Split(new char[] {',', ' '}, StringSplitOptions.RemoveEmptyEntries);

            // Consecutive number of a JSON object openings always match number of closings.
            // This makes it easier to check if there are valid JSONs in the message.
            char[] openings = { '{', '[' };
            char[] closings = { ']', '}' };
            int stack = 0;
            bool dirty = false;
            var reg = new StringBuilder();
            for (int i = 0; i < message.Length; i++)
            {
                var @char = message[i];

                if (openings.Contains(@char))
                {
                    dirty = true;
                    ++stack;
                }
                else if (dirty && closings.Contains(@char))
                    --stack;

                if (dirty)
                {
                    reg.Append(@char);
                    if (stack == 0)
                    {
                        // Check JSON
                        try
                        {
                            var json = JObject.Parse(Regex.Unescape(reg.ToString()));
                            foreach (var jToken in json.DescendantsAndSelf())
                            {
                                if (jToken is JObject obj)
                                    foreach (var prop in obj.Properties())
                                        if (prop.Value is not JObject && prop.Value is not JArray)
                                            if (search.Contains(prop.Name, StringComparer.OrdinalIgnoreCase))
                                                prop.Value = _mask;
                            }

                            // Convert the edited JSON back to string and write it to the stream.
                            writer.Write(json.ToString(Newtonsoft.Json.Formatting.None));
                        }
                        catch (Exception)
                        {
                            // Invalid JSON, dump reg on stream
                            writer.Write(reg);
                        }

                        dirty = false;
                        reg.Clear();
                    }
                }
                else
                    writer.Write(@char); // no JSON
            }

            if (dirty)
                writer.Write(reg);

            reg.Clear();

            writer.WriteLine();
        }
    }
}

代码通过检查JSON对象有效所必须匹配的开头和结尾的数量来探测JSON对象。只要记录消息中的当前字符没有构建JSON,就会立即将其写入流中。否则,将使用StringBuilder来保存可能是JSON的字符,并在其完成时,它被临时转换为JObject,JObject又被遍历以屏蔽(替换)或隐藏敏感属性的所有值。
此自定义布局可用于log4net配置如下的情况:

<appender name="FileAppender" type="log4net.Appender.FileAppender">
    <file value="C:\Users\user\AppData\Roaming\log2.txt" />
    <appendToFile value="true" />
    <layout type="Company.Project.Logging.MaskLayout">
        <search value="password,pwd" />
        <mask value="***" />
    </layout>
</appender>

事实上,所有的日志框架都应该支持实现拦截器,就像前面讨论的那样,我希望这能节省一些人的时间。

相关问题