Search code examples
c#.netasp.net-mvcnlog.net-4.8

How to mask sensetive data for particular requests (NLog)


Some of my actions accept models like:

    public class PaymentRequest
    {
        public decimal Amount { get; set; }
        public bool? SaveCard { get; set; }
        public int? SmsCode { get; set; }
        public BankCardDetails Card { get; set; }
    }

    public class BankCardDetails
    {
        public string Number { get; set; }
        public string HolderName { get; set; }
        public string ExpiryDate { get; set; }
        public string ValidationCode { get; set; }
    }

And the action method looks like:

        [HttpPost]
        [Route("api/v1/payment/pay")]
        public Task<BankCardActionResponse> Pay([FromBody] PaymentRequest request)
        {
            if (request == null)
                throw new HttpResponseException(HttpStatusCode.BadRequest);

            return _paymentService.PayAsync(DataUserHelper.PhoneNumber, request);
        }

I use Nlog. I think it's clear this is a bad idea to log all this bank data. My log config file contained the following line:

<attribute name="user-requestBody" layout="${aspnet-request-posted-body}"/>

I logged the request. I decided to refactor that and planned the following strategy. Actions that contain sensitive data into their requests I will mark with an attribute like

 [RequestMethodFormatter(typeof(PaymentRequest))]

then take a look at my custom renderer:

    [LayoutRenderer("http-request")]
    public class NLogHttpRequestLayoutRenderer : AspNetRequestPostedBody
    {
        protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent)
        {
            base.DoAppend(builder, logEvent);

            var body = builder.ToString();
            
            // Get attribute of the called action. 
            var type = ... // How can I get "PaymentRequest" from the [RequestMethodFormatter(typeof(PaymentRequest))] 
            var res = MaskHelper.GetMaskedJsonString(body, type);
           
           
            // ... and so on
        }
    }

I think you understand the idea. I need the type from the method's RequestMethodFormatter attribute. Is it even possible to get it into the renderer? I need it because I'm going to deserialize request JSON into particular models (it's gonna be into the MaskHelper.GetMaskedJsonString), work with the models masking the data, serialize it back into JSON.

So, did I choose a wrong approach? Or it's possible to get the type from the attribute into the renderer?


Solution

  • After some research, I ended up with the following solution:

    
    namespace ConsoleApp7
    {
        internal class Program
        {
            private static void Main()
            {
                var sourceJson = GetSourceJson();
                var userInfo = JsonConvert.DeserializeObject(sourceJson, typeof(User));
    
                Console.WriteLine("----- Serialize without Resolver-----");
                Console.WriteLine(JsonConvert.SerializeObject(userInfo));
    
                Console.WriteLine("----- Serialize with Resolver-----");
                Console.WriteLine(JsonConvert.SerializeObject(userInfo, new JsonSerializerSettings
                {
                    ContractResolver = new MaskPropertyResolver()
                }));
            }
    
            private static string GetSourceJson()
            {
                var guid = Guid.Parse("3e92f0c4-55dc-474b-ae21-8b3dac1a0942");
                return JsonConvert.SerializeObject(new User
                {
                    UserId = guid,
                    Age = 19,
                    Name = "John",
                    BirthDate = new DateTime(1990, 5, 12),
                    Hobbies = new[]
                    {
                        new Hobby
                        {
                            Name = "Football",
                            Rating = 5,
                            DurationYears = 3,
                        },
                        new Hobby
                        {
                            Name = "Basketball",
                            Rating = 7,
                            DurationYears = 4,
                        }
                    }
                });
            }
        }
    
        public class User
        {
            [MaskGuidValue]
            public Guid UserId { get; set; }
            [MaskStringValue("***")] public string Name { get; set; }
            public int Age { get; set; }
            [MaskDateTimeValue]
            public DateTime BirthDate { get; set; }
    
            public Hobby[] Hobbies { get; set; }
        }
    
        public class Hobby
        {
            [MaskStringValue("----")] 
            public string Name { get; set; }
    
            [MaskIntValue(replacement: 11111)]
            public int Rating { get; set; }
    
            public int DurationYears { get; set; }
        }
    
        public class MaskPropertyResolver : DefaultContractResolver
        {
            protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
            {
                var props = base.CreateProperties(type, memberSerialization);
                var allowedPropertyTypes = new Type[]
                {
                    typeof(Guid),
                    typeof(DateTime),
                    typeof(string),
                    typeof(int),
                };
    
                foreach (var prop in props.Where(p => allowedPropertyTypes.Contains(p.PropertyType)))
                {
                    if (prop.UnderlyingName == null)
                        continue;
    
                    var propertyInfo = type.GetProperty(prop.UnderlyingName);
                    var attribute =
                        propertyInfo?.GetCustomAttributes().FirstOrDefault(x => x is IMaskAttribute) as IMaskAttribute;
    
                    if (attribute == null)
                    {
                        continue;
                    }
    
                    if (attribute.Type != propertyInfo.PropertyType)
                    {
                        // Log this case, cause somebody used wrong attribute
                        continue;
                    }
                    
                    prop.ValueProvider = new MaskValueProvider(propertyInfo, attribute.Replacement, attribute.Type);
                }
    
                return props;
            }
    
            private class MaskValueProvider : IValueProvider
            {
                private readonly PropertyInfo _targetProperty;
                private readonly object _replacement;
                private readonly Type _type;
    
                public MaskValueProvider(PropertyInfo targetProperty, object replacement, Type type)
                {
                    _targetProperty = targetProperty;
                    _replacement = replacement;
                    _type = type;
                }
    
                public object GetValue(object target)
                {
                    return _replacement;
                }
    
                public void SetValue(object target, object value)
                {
                    _targetProperty.SetValue(target, value);
                }
            }
        }
        
    
        [AttributeUsage(AttributeTargets.Property)]
        public class MaskStringValueAttribute : Attribute, IMaskAttribute
        {
            public Type Type => typeof(string);
            public object Replacement { get; }
    
            public MaskStringValueAttribute(string replacement)
            {
                Replacement = replacement;
            }
        }
    
        [AttributeUsage(AttributeTargets.Property)]
        public class MaskIntValueAttribute : Attribute, IMaskAttribute
        {
            public object Replacement { get; }
            public Type Type => typeof(int);
    
            public MaskIntValueAttribute(int replacement)
            {
                Replacement = replacement;
            }
        }
    
        [AttributeUsage(AttributeTargets.Property)]
        public class MaskGuidValueAttribute : Attribute, IMaskAttribute
        {
            public Type Type => typeof(Guid);
            public object Replacement => Guid.Empty;
        }
    
    
        [AttributeUsage(AttributeTargets.Property)]
        public class MaskDateTimeValueAttribute : Attribute, IMaskAttribute
        {
            public Type Type => typeof(DateTime);
            public object Replacement => new DateTime(1970, 1, 1);
        }
    
        public interface IMaskAttribute
        {
            Type Type { get; }
            object Replacement { get; }
        }
    }
    
    

    I hope somebody will find it helpful.