MetalamaCommented examplesLoggingStep 7.​ Removing sensitive information
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 = this._logger.IsEnabled(LogLevel.Trace);
8        if (isTracingEnabled)
9        {
10            LoggerExtensions.LogTrace(this._logger, $"LoginService.VerifyPassword(account = {{{account}}}, password = <redacted> ) started.");
11        }
12
13        try
14        {
15            bool result;
16            result = account == password;if (isTracingEnabled)
17            {
18                LoggerExtensions.LogTrace(this._logger, $"LoginService.VerifyPassword(account = {{{account}}}, password = <redacted> ) returned {result}.");
19            }
20
21            return result;
22        }
23        catch (Exception e) when (this._logger.IsEnabled(LogLevel.Warning))
24        {
25            LoggerExtensions.LogWarning(this._logger, $"LoginService.VerifyPassword(account = {{{account}}}, password = <redacted> ) failed: {e.Message}");
26            throw;
27        }
28    }
29
30    [return: NotLogged]
31    public string GetSaltedHash( string account, string password, [NotLogged] string salt ) {
32var isTracingEnabled = this._logger.IsEnabled(LogLevel.Trace);
33        if (isTracingEnabled)
34        {
35            LoggerExtensions.LogTrace(this._logger, $"LoginService.GetSaltedHash(account = {{{account}}}, password = <redacted> , salt = <redacted> ) started.");
36        }
37
38        try
39        {
40            string result;
41            result =         account + password + salt;if (isTracingEnabled)
42            {
43                LoggerExtensions.LogTrace(this._logger, $"LoginService.GetSaltedHash(account = {{{account}}}, password = <redacted> , salt = <redacted> ) returned <redacted>.");
44            }
45
46            return result;
47        }
48        catch (Exception e) when (this._logger.IsEnabled(LogLevel.Warning))
49        {
50            LoggerExtensions.LogWarning(this._logger, $"LoginService.GetSaltedHash(account = {{{account}}}, password = <redacted> , salt = <redacted> ) failed: {e.Message}");
51            throw;
52        }
53    }
54private ILogger _logger;
55
56    public LoginService
57(ILogger<LoginService> logger = default(global::Microsoft.Extensions.Logging.ILogger<global::LoginService>))
58    {
59        this._logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
60    }
61}

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 = new[] { "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
3{
4}

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.Is( 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( meta.Target.Method.ReturnParameter ) )
48                    {
49                        successMessage.AddText( "<redacted>" );
50                    }
51                    else
52                    {
53                        successMessage.AddExpression( result );
54                    }
55
56                    successMessage.AddText( "." );
57                }
58
59                this._logger.LogTrace( (string) successMessage.ToValue() );
60            }
61
62            return result;
63        }
64        catch ( Exception e ) when ( this._logger.IsEnabled( LogLevel.Warning ) )
65        {
66            // Display the failure message.
67            var failureMessage = BuildInterpolatedString( false );
68            failureMessage.AddText( " failed: " );
69            failureMessage.AddExpression( e.Message );
70            this._logger.LogWarning( (string) failureMessage.ToValue() );
71
72            throw;
73        }
74    }
75
76    // Builds an InterpolatedStringBuilder with the beginning of the message.
77    private static InterpolatedStringBuilder BuildInterpolatedString( bool includeOutParameters )
78    {
79        var stringBuilder = new InterpolatedStringBuilder();
80
81        // Include the type and method name.
82        stringBuilder.AddText( meta.Target.Type.ToDisplayString( CodeDisplayFormat.MinimallyQualified ) );
83        stringBuilder.AddText( "." );
84        stringBuilder.AddText( meta.Target.Method.Name );
85        stringBuilder.AddText( "(" );
86        var i = meta.CompileTime( 0 );
87
88        // Include a placeholder for each parameter.
89        foreach ( var p in meta.Target.Parameters )
90        {
91            var comma = i > 0 ? ", " : "";
92
93            if ( SensitiveParameterFilter.IsSensitive( p ) )
94            {
95                // Do not log sensitive parameters.
96                stringBuilder.AddText( $"{comma}{p.Name} = <redacted> " );
97            }
98            else if ( p.RefKind == RefKind.Out && !includeOutParameters )
99            {
100                // When the parameter is 'out', we cannot read the value.
101                stringBuilder.AddText( $"{comma}{p.Name} = <out> " );
102            }
103            else
104            {
105                // Otherwise, add the parameter value.
106                stringBuilder.AddText( $"{comma}{p.Name} = {{" );
107                stringBuilder.AddExpression( p.Value );
108                stringBuilder.AddText( "}" );
109            }
110
111            i++;
112        }
113
114        stringBuilder.AddText( ")" );
115
116        return stringBuilder;
117    }
118}
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.