Ever run into problems where you really want to see exactly what the user submitted to your web application? I do, especially when I lack a one-to-one relationship between the view and model. This seems like something an action filter should be able to do for me. And as you might have gathered from the title, logging posted form values is something that Rails does out of the box.
There’s a security concern here that we need to concern ourselves with. We’re usually smart enough to encrypt passwords, credit card numbers, and other sensitive information in our databases. And if you’re not encrypting that stuff, you really should be. Like now. We don’t want to introduce a new security hole in our application by logging that information in our database in our logging table. Who cares if your user table is protected if I can just strip the passwords from your log history. This is, obviously, not ideal.
public class LogRequestFormAttribute : ActionFilterAttribute
{
private static readonly string[] defaultProtectedFields = new[] { "CreditCard", "CreditCardNumber", "NewPassword", "NewPasswordConfirmation", "Password", "PasswordConfirmation" };
private static string[] protectedFields;
static LogRequestFormAttribute()
{
Bus.Instance.AddMessageHandlerType(typeof(DebugLogRequestFormMessageObserver));
}
public static IEnumerable<string> ProtectedFormFields
{
get { return protectedFields ?? (protectedFields = ConfigProtectedFormFields.Union(defaultProtectedFields).Distinct().ToArray()); }
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Bus.Instance.Send(new LogRequestFormMessage(filterContext));
}
private static IEnumerable<string> ConfigProtectedFormFields
{
get
{
string protectedFormFieldsString = ConfigurationManager.AppSettings["ProtectedFormFields"];
if (string.IsNullOrEmpty(protectedFormFieldsString)) return new string[] { };
var protectedFormFields = protectedFormFieldsString
.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim());
return protectedFormFields;
}
}
}
Right off the bat, I am making sure that form fields called CreditCard, CreditCardNumber, NewPassword, NewPasswordConfirmation, Password, or PasswordConfirmation are listed as protected. Seriously, I don’t care what what your configuration says.
Obviously, this is something Microsoft would never do. Microsoft builds a framework. If you want to use that framework in a hazardous and potential unsecure way, then that’s your problem. Me? I’m not bound by the same constraints.
I do recognize that you may have other values that you want kept out of your logs. Have no fear: it’s easy enough to configure as part of your web.config file.
<add key="ProtectedFormFields" value="CCNumber" />
And with that, we’re now ensuring that anytime a form is posted, any field name CCNumber will be obscured in the message. The trick is in the message. (Don’t worry, it’s not much of a trick.)
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine(string.Format("Url: {0}, Controller: {1}, Action: {2}", Url, ControllerName, ActionName));
var keyStrings = new List();
foreach (string key in FormData.AllKeys)
{
var value = GetFormValue(key);
var keyString = string.Format("{0}: {1}", key, value);
keyStrings.Add(keyString);
}
sb.AppendLine(string.Join(", ", keyStrings));
return sb.ToString();
}
internal string GetFormValue(string key)
{
string value;
if (IsProtected(key))
{
var valueLength = (FormData[key] ?? "").Length;
value = new string('*', valueLength);
}
else
{
value = FormData[key] ?? "";
}
return value;
}
What’s Up With The Bus?
The bus is a super simple in-memory message bus infrastructure to send events. It’s part of the Curiosity.Common library. Feel free to check it out!
The DebugLogRequestFormMessageObserver
The debug observer will write to the Visual Studio debug output window. You’ll get something like this.
Url: /home/testform, Controller: home, Action: testform FirstName: Jarrett, LastName: Meyer, Password: **************, PasswordConfirmation: **************, SSN: ***********
Adding Your Own Handler
Want to use log4net? It’s easy enough to add your own message handler.
public class Log4NetLogRequestFormMessageObserver : MessageHandlerBase{ private readonly ILog log = LogManager.GetLogger("form-logger"); public override void Handle(LogRequestFormMessage message) { log.Debug(message); } }
Simple!
Now With More NuGet Awesome
This awesome new feature has been added to Curiosity.Common.Mvc (version 1.8), and is available on NuGet. For more info, check out the wiki page on GitHub.