0

I am logging my parameter models on console. In there I don't want to display the user password on login/register raw in logs, I want *. My limitation is that I need to use System.Text.Json! This is what I tried.

[AttributeUsage(AttributeTargets.Property)]
public class SensitiveDataAttribute : JsonAttribute
{
}

public class LoginModel
{
    public string? Email { get; set; }

    [SensitiveData]
    public string? Password { get; set; }

    public override string ToString() => this.ToLogJsonString();
}

public class SensitiveDataConverter : JsonConverter<string>
{
    public SensitiveDataConverter()
    {
    }

    public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return reader.GetString();
    }

    public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
    {
        var attribute = value.GetType().GetTypeInfo().GetCustomAttribute<SensitiveDataAttribute>();

        if (attribute is null)
        {
            writer.WriteStringValue(value);
            return;
        }

        var secret = new String(value.Select(x => '*').ToArray());
        writer.WriteStringValue(secret);
    }
}

public static class LoggableObjectExtensions
{
    public static string ToLogJsonString(this object value)
    {
        JsonSerializerOptions options = new JsonSerializerOptions
        {
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
            PropertyNameCaseInsensitive = true,
            WriteIndented = false,
        };

        options.Converters.Add(new SensitiveDataConverter());

        return JsonSerializer.Serialize(value, options);

    }
}

At the end wanted to see the result

var model = new LoginModel
{
    Email = "test@test.com",
    Password = "Abrakadabra"
};


Console.WriteLine(model.ToLogJsonString());

I problem is that my attribute not got recognized in the SensitiveDataConverter. Any idea? thnx

Wasyster
  • 2,279
  • 4
  • 26
  • 58
  • Can't you just write a [custom type converter](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonconverterattribute?view=net-6.0) and apply it to those properties? – Kirk Woll May 12 '22 at 19:04
  • I can't, because I do not want to convert every string field, just one that holds the password. – Wasyster May 12 '22 at 19:17
  • 1
    Right, that's why I said, "and apply it to those properties". `[JsonConverter(typeof(SensitiveData))]public string? Password { get; set; }` – Kirk Woll May 12 '22 at 19:22
  • That will always replace my password value, and I do not want that. – Wasyster May 12 '22 at 19:53
  • Sure, that is a good clarification, which is what I was looking for. – Kirk Woll May 12 '22 at 20:08
  • With Newtonsoft one would use a [custom contract resolver](https://www.newtonsoft.com/json/help/html/contractresolver.htm#CustomIContractResolverExamples) to apply the necessary logic when `[SensitiveData]` is present, but unfortunately as of .NET 6 System.Text.Json has no public API equivalent to `DefaultContractResolver`. See: [System.Text.Json API is there something like IContractResolver](https://stackoverflow.com/q/58926112/3744182). – dbc May 12 '22 at 20:39
  • By the way, if you simply replace each character of the password with `*` you are leaking some sensitive data, namely the **length** of the password. Knowing that, attackers can more easily guess the password. – dbc May 12 '22 at 20:42
  • Would you be willing to use, say, some thread-static flag to indicate that sensitive data should be redacted? The thread static flag would be made to work in conjunction with some custom converter applied to sensitive data properties. – dbc May 12 '22 at 20:58

1 Answers1

0

the problem is that in your Write method public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)

the argument value has no knowledge about LoginModel. You are doing a reflection on the primitive string type to try and read the attribute that is in LoginModel. Instead you need to have your Converter tied to the LoginModel so you can inspect it's properties

public class SensitiveDataConverter : JsonConverter<LoginModel>
{
    public override LoginModel? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, LoginModel value, JsonSerializerOptions options)
    {
        foreach (var propInfo in value.GetType().GetProperties())
        {
            if (propInfo.CanRead)
            {
                var propVal = propInfo.GetValue(value, null);

                // obviously, leaving it up to you to beautify your json :)
                if (propInfo.GetCustomAttribute<SensitiveDataAttribute>() != null)
                {
                    writer.WriteStringValue("SensitiveData");
                }
                else
                {
                    writer.WriteStringValue(propVal.ToString());
                }
            }
        }
    }
}
CodeReaper
  • 775
  • 2
  • 6
  • 21
  • downside, you need a converter for each class that has sensitive data :( – CodeReaper May 12 '22 at 21:10
  • @Wasyster you asked "problem is that my attribute not got recognized in the SensitiveDataConverter, Any idea?". Does my answer explain why that was happening in your code? I also provide a solution that meets your requirements of only masking data when logging to console. If you are happy, can you mark this as Accepted. Thanks – CodeReaper May 25 '22 at 13:34