Open sandboxFocusImprove this doc

Logging example, step 7: Removing sensitive data

Ensuring the security and privacy of sensitive data is a critical responsibility for developers. Logs can inadvertently expose sensitive information, especially if all methods and their parameter values are logged without review. This example shows how to prevent logging specific parameters, mitigating the risk of data breaches and unauthorized access.

In the following examples, the password and the salt are excluded from the log:

Source Code



1public class LoginService
2{
3    // The 'password' parameter will not be logged because of its name.
4    public bool VerifyPassword( string account, string password ) => account == password;






















5
6    [return: NotLogged]
7    public string GetSaltedHash( string account, string password, [NotLogged] string salt )
8        => account + password + salt;




























9}
Transformed Code
1using System;
2using Microsoft.Extensions.Logging;
3
4public class LoginService
5{
6    // The 'password' parameter will not be logged because of its name.
7    public bool VerifyPassword( string account, string password ) { var isTracingEnabled = _logger.IsEnabled(LogLevel.Trace);
8        if (isTracingEnabled)
9        {
10            _logger.LogTrace($"LoginService.VerifyPassword(account = {{{account}}}, password = <redacted> ) started.");
11        }
12
13        try
14        {
15            bool result;
16            result = account == password;
17            if (isTracingEnabled)
18            {
19                _logger.LogTrace($"LoginService.VerifyPassword(account = {{{account}}}, password = <redacted> ) returned {result}.");
20            }
21
22            return (bool)result;
23        }
24        catch (Exception e) when (_logger.IsEnabled(LogLevel.Warning))
25        {
26            _logger.LogWarning($"LoginService.VerifyPassword(account = {{{account}}}, password = <redacted> ) failed: {e.Message}");
27            throw;
28        }
29    }
30
31    [return: NotLogged]
32    public string GetSaltedHash( string account, string password, [NotLogged] string salt )
33        { var isTracingEnabled = _logger.IsEnabled(LogLevel.Trace);
34        if (isTracingEnabled)
35        {
36            _logger.LogTrace($"LoginService.GetSaltedHash(account = {{{account}}}, password = <redacted> , salt = <redacted> ) started.");
37        }
38
39        try
40        {
41            string result;
42            result = account + password + salt;
43            if (isTracingEnabled)
44            {
45                _logger.LogTrace($"LoginService.GetSaltedHash(account = {{{account}}}, password = <redacted> , salt = <redacted> ) returned <redacted>.");
46            }
47
48            return (string)result;
49        }
50        catch (Exception e) when (_logger.IsEnabled(LogLevel.Warning))
51        {
52            _logger.LogWarning($"LoginService.GetSaltedHash(account = {{{account}}}, password = <redacted> , salt = <redacted> ) failed: {e.Message}");
53            throw;
54        }
55    }
56private ILogger _logger;
57
58    public LoginService(ILogger<LoginService> logger = null)
59    {
60        this._logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
61    }
62}

Implementation

Parameters are filtered by the following compile-time class:

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3
4[CompileTime]
5internal static class SensitiveParameterFilter
6{
7    private static readonly string[] _sensitiveNames = ["password", "credential", "pwd"];
8
9    public static bool IsSensitive( IParameter parameter )
10    {
11        if ( parameter.Attributes.OfAttributeType( typeof(NotLoggedAttribute) ).Any() )
12        {
13            return true;
14        }
15
16        if ( _sensitiveNames.Any( n => parameter.Name.ToLowerInvariant().Contains( n ) ) )
17        {
18            return true;
19        }
20
21        return false;
22    }
23}

As shown, the IsSensitive method must determine whether a parameter is sensitive. It bases its decision on two factors: if the parameter name contains well-known keywords or if the parameter is explicitly annotated with the [NotLogged] custom attribute, which we just defined for this project.

1[AttributeUsage( AttributeTargets.Parameter | AttributeTargets.ReturnValue )]
2public sealed class NotLoggedAttribute : Attribute { }

The LogAttribute aspect has been modified to call SensitiveParameterFilter.IsSensitive and use the text <redacted> instead of the parameter value for sensitive parameters.

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Code.SyntaxBuilders;
5using Microsoft.Extensions.Logging;
6
7#pragma warning disable CS8618, CS0649
8
9public class LogAttribute : OverrideMethodAspect
10{
11    [IntroduceDependency]
12    private readonly ILogger _logger;
13
14    public override dynamic? OverrideMethod()
15    {
16        // Determine if tracing is enabled.
17        var isTracingEnabled = this._logger.IsEnabled( LogLevel.Trace );
18
19        // Write entry message.
20        if ( isTracingEnabled )
21        {
22            var entryMessage = BuildInterpolatedString( false );
23            entryMessage.AddText( " started." );
24            this._logger.LogTrace( (string) entryMessage.ToValue() );
25        }
26
27        try
28        {
29            // Invoke the method and store the result in a variable.
30            var result = meta.Proceed();
31
32            if ( isTracingEnabled )
33            {
34                // Display the success message. The message is different when the method is void.
35                var successMessage = BuildInterpolatedString( true );
36
37                if ( meta.Target.Method.ReturnType.Equals( typeof(void) ) )
38                {
39                    // When the method is void, display a constant text.
40                    successMessage.AddText( " succeeded." );
41                }
42                else
43                {
44                    // When the method has a return value, add it to the message.
45                    successMessage.AddText( " returned " );
46
47                    if ( SensitiveParameterFilter.IsSensitive(
48                            meta.Target.Method
49                                .ReturnParameter ) )
50                    {
51                        successMessage.AddText( "<redacted>" );
52                    }
53                    else
54                    {
55                        successMessage.AddExpression( result );
56                    }
57
58                    successMessage.AddText( "." );
59                }
60
61                this._logger.LogTrace( (string) successMessage.ToValue() );
62            }
63
64            return result;
65        }
66        catch ( Exception e ) when ( this._logger.IsEnabled( LogLevel.Warning ) )
67        {
68            // Display the failure message.
69            var failureMessage = BuildInterpolatedString( false );
70            failureMessage.AddText( " failed: " );
71            failureMessage.AddExpression( e.Message );
72            this._logger.LogWarning( (string) failureMessage.ToValue() );
73
74            throw;
75        }
76    }
77
78    // Builds an InterpolatedStringBuilder with the beginning of the message.
79    private static InterpolatedStringBuilder BuildInterpolatedString( bool includeOutParameters )
80    {
81        var stringBuilder = new InterpolatedStringBuilder();
82
83        // Include the type and method name.
84        stringBuilder.AddText( meta.Target.Type.ToDisplayString( CodeDisplayFormat.MinimallyQualified ) );
85        stringBuilder.AddText( "." );
86        stringBuilder.AddText( meta.Target.Method.Name );
87        stringBuilder.AddText( "(" );
88        var i = 0;
89
90        // Include a placeholder for each parameter.
91        foreach ( var p in meta.Target.Parameters )
92        {
93            var comma = i > 0 ? ", " : "";
94
95            if ( SensitiveParameterFilter.IsSensitive( p ) )
96            {
97                // Do not log sensitive parameters.
98                stringBuilder.AddText( $"{comma}{p.Name} = <redacted> " );
99            }
100            else if ( p.RefKind == RefKind.Out && !includeOutParameters )
101            {
102                // When the parameter is 'out', we cannot read the value.
103                stringBuilder.AddText( $"{comma}{p.Name} = <out> " );
104            }
105            else
106            {
107                // Otherwise, add the parameter value.
108                stringBuilder.AddText( $"{comma}{p.Name} = {{" );
109                stringBuilder.AddExpression( p );
110                stringBuilder.AddText( "}" );
111            }
112
113            i++;
114        }
115
116        stringBuilder.AddText( ")" );
117
118        return stringBuilder;
119    }
120}
Warning

This approach does not guarantee that there will be no leak of sensitive data to logs because it relies on manual identification of parameters by you, the aspect's developer, or by the aspect's users, which is subject to human error. To verify that you have not forgotten anything, consider the following strategies:

  • Do not pass sensitive data in strings, but wrap them into an object and do not expose sensitive data in the implementation of the ToString method of this wrapping class.
  • Perform tests by injecting well-known strings as values for sensitive parameters (e.g., p@ssw0rd), enable logging to the maximum verbosity, and verify that the logs do not contain any of the well-known values. These tests must have complete coverage to be accurate.