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:
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}
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;if (isTracingEnabled)
17 {
18 _logger.LogTrace($"LoginService.VerifyPassword(account = {{{account}}}, password = <redacted> ) returned {result}.");
19 }
20
21 return result;
22 }
23 catch (Exception e) when (_logger.IsEnabled(LogLevel.Warning))
24 {
25 _logger.LogWarning($"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 = _logger.IsEnabled(LogLevel.Trace);
33 if (isTracingEnabled)
34 {
35 _logger.LogTrace($"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 _logger.LogTrace($"LoginService.GetSaltedHash(account = {{{account}}}, password = <redacted> , salt = <redacted> ) returned <redacted>.");
44 }
45
46 return result;
47 }
48 catch (Exception e) when (_logger.IsEnabled(LogLevel.Warning))
49 {
50 _logger.LogWarning($"LoginService.GetSaltedHash(account = {{{account}}}, password = <redacted> , salt = <redacted> ) failed: {e.Message}");
51 throw;
52 }
53 }
54private ILogger _logger;
55
56 public LoginService(ILogger<LoginService> logger = default(global::Microsoft.Extensions.Logging.ILogger<global::LoginService>))
57 {
58 this._logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
59 }
60}
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] private readonly ILogger _logger;
12
13 public override dynamic? OverrideMethod()
14 {
15 // Determine if tracing is enabled.
16 var isTracingEnabled = this._logger.IsEnabled(LogLevel.Trace);
17
18 // Write entry message.
19 if (isTracingEnabled)
20 {
21 var entryMessage = BuildInterpolatedString(false);
22 entryMessage.AddText(" started.");
23 this._logger.LogTrace((string)entryMessage.ToValue());
24 }
25
26 try
27 {
28 // Invoke the method and store the result in a variable.
29 var result = meta.Proceed();
30
31 if (isTracingEnabled)
32 {
33 // Display the success message. The message is different when the method is void.
34 var successMessage = BuildInterpolatedString(true);
35
36 if (meta.Target.Method.ReturnType.Is(typeof(void)))
37 {
38 // When the method is void, display a constant text.
39 successMessage.AddText(" succeeded.");
40 }
41 else
42 {
43 // When the method has a return value, add it to the message.
44 successMessage.AddText(" returned ");
45
46 if (SensitiveParameterFilter.IsSensitive(meta.Target.Method
47 .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(
83 meta.Target.Type.ToDisplayString(CodeDisplayFormat.MinimallyQualified));
84 stringBuilder.AddText(".");
85 stringBuilder.AddText(meta.Target.Method.Name);
86 stringBuilder.AddText("(");
87 var i = 0;
88
89 // Include a placeholder for each parameter.
90 foreach (var p in meta.Target.Parameters)
91 {
92 var comma = i > 0 ? ", " : "";
93
94 if (SensitiveParameterFilter.IsSensitive(p))
95 {
96 // Do not log sensitive parameters.
97 stringBuilder.AddText($"{comma}{p.Name} = <redacted> ");
98 }
99 else if (p.RefKind == RefKind.Out && !includeOutParameters)
100 {
101 // When the parameter is 'out', we cannot read the value.
102 stringBuilder.AddText($"{comma}{p.Name} = <out> ");
103 }
104 else
105 {
106 // Otherwise, add the parameter value.
107 stringBuilder.AddText($"{comma}{p.Name} = {{");
108 stringBuilder.AddExpression(p);
109 stringBuilder.AddText("}");
110 }
111
112 i++;
113 }
114
115 stringBuilder.AddText(")");
116
117 return stringBuilder;
118 }
119}
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.