I've done some security fixes on the version 3.50.
Now my question is whether this changes can be included in the future releases of nopCommerce.
Issue: Preventing open redirection attacks
File: /Presentation/Nop.Web/Administration/Controllers/CommonController.cs
Code:
public ActionResult SetLanguage(int langid, string returnUrl = "")
{
var language = _languageService.GetLanguageById(langid);
if (language != null)
{
_workContext.WorkingLanguage = language;
}
//home page
// Preventing open redirection attacks
if (String.IsNullOrEmpty(returnUrl) || !Url.IsLocalUrl(returnUrl))
returnUrl = Url.Action("Index", "Home", new { area = "Admin" });
return Redirect(returnUrl);
}
Issue: Broken Authentication and Session Management
File: /Libraries/Nop.Services/Authentication/FormsAuthenticationService.cs
Description: To prevent cookie replay attacks, I generate a random guid and I store it in the user data section and in the session
Code:
public partial class FormsAuthenticationService : IAuthenticationService
{
private readonly HttpContextBase _httpContext;
private readonly ICustomerService _customerService;
private readonly CustomerSettings _customerSettings;
private readonly TimeSpan _expirationTimeSpan;
private Customer _cachedCustomer;
/// <summary>
/// Ctor
/// </summary>
/// <param name="httpContext">HTTP context</param>
/// <param name="customerService">Customer service</param>
/// <param name="customerSettings">Customer settings</param>
public PostDocAuthenticationService(HttpContextBase httpContext,
ICustomerService customerService, CustomerSettings customerSettings)
{
this._httpContext = httpContext;
this._customerService = customerService;
this._customerSettings = customerSettings;
this._expirationTimeSpan = FormsAuthentication.Timeout;
}
public virtual void SignIn(Customer customer, bool createPersistentCookie)
{
var now = DateTime.UtcNow.ToLocalTime();
// To prevent cookie replay attacks, I generate a random guid and I store it in the user data section and in the session
var userGuid = Guid.NewGuid();
var ticket = new FormsAuthenticationTicket(
1 /*version*/,
_customerSettings.UsernamesEnabled ? customer.Username : customer.Email,
now,
now.Add(_expirationTimeSpan),
createPersistentCookie,
_customerSettings.UsernamesEnabled ? customer.Username + "_" + userGuid : customer.Email + "_" + userGuid,
FormsAuthentication.FormsCookiePath);
var encryptedTicket = FormsAuthentication.Encrypt(ticket);
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
cookie.HttpOnly = true;
if (ticket.IsPersistent)
{
cookie.Expires = ticket.Expiration;
}
cookie.Secure = FormsAuthentication.RequireSSL;
cookie.Path = FormsAuthentication.FormsCookiePath;
if (FormsAuthentication.CookieDomain != null)
{
cookie.Domain = FormsAuthentication.CookieDomain;
}
_httpContext.Response.Cookies.Add(cookie);
_cachedCustomer = customer;
// Store in session the random guid to prevent cookie replay attacks
if (HttpContext.Current != null && HttpContext.Current.Session != null)
{
HttpContext.Current.Session["authID"] = userGuid;
}
if (_httpContext.Session != null)
{
_httpContext.Session["authID"] = userGuid;
}
}
public virtual void SignOut()
{
_cachedCustomer = null;
if (_httpContext != null && _httpContext.Session != null)
_httpContext.Session.Abandon();
FormsAuthentication.SignOut();
}
public virtual Customer GetAuthenticatedCustomer()
{
if (_cachedCustomer != null)
return _cachedCustomer;
if (_httpContext == null ||
_httpContext.Request == null ||
!_httpContext.Request.IsAuthenticated ||
!(_httpContext.User.Identity is FormsIdentity))
{
return null;
}
var formsIdentity = (FormsIdentity)_httpContext.User.Identity;
var customer = GetAuthenticatedCustomerFromTicket(formsIdentity.Ticket);
if (customer != null && customer.Active && !customer.Deleted && customer.IsRegistered())
{
_cachedCustomer = customer;
}
return _cachedCustomer;
}
public virtual Customer GetAuthenticatedCustomerFromTicket(FormsAuthenticationTicket ticket)
{
if (ticket == null)
throw new ArgumentNullException("ticket");
var usernameOrEmail = ticket.UserData;
// User data section must contain the random guid generated during the login
if (!usernameOrEmail.Contains("_"))
{
return null;
}
usernameOrEmail = usernameOrEmail.Split('_')[0];
if (String.IsNullOrWhiteSpace(usernameOrEmail))
{
return null;
}
var customer = _customerSettings.UsernamesEnabled
? _customerService.GetCustomerByUsername(usernameOrEmail)
: _customerService.GetCustomerByEmail(usernameOrEmail);
return customer;
}
}
File: /Presentation/Nop.Web.Framework/Controllers/AdminAuthorizeAttribute.cs
Description: Check the random guid saved in the session
Code:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=true)]
public class AdminAuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
private readonly bool _dontValidate;
public AdminAuthorizeAttribute()
: this(false)
{
}
public AdminAuthorizeAttribute(bool dontValidate)
{
this._dontValidate = dontValidate;
}
private void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new HttpUnauthorizedResult();
}
private IEnumerable<AdminAuthorizeAttribute> GetAdminAuthorizeAttributes(ActionDescriptor descriptor)
{
return descriptor.GetCustomAttributes(typeof(AdminAuthorizeAttribute), true)
.Concat(descriptor.ControllerDescriptor.GetCustomAttributes(typeof(AdminAuthorizeAttribute), true))
.OfType<AdminAuthorizeAttribute>();
}
private bool IsAdminPageRequested(AuthorizationContext filterContext)
{
var adminAttributes = GetAdminAuthorizeAttributes(filterContext.ActionDescriptor);
if (adminAttributes != null && adminAttributes.Any())
return true;
return false;
}
public void OnAuthorization(AuthorizationContext filterContext)
{
if (_dontValidate)
return;
if (filterContext == null)
throw new ArgumentNullException("filterContext");
if (OutputCacheAttribute.IsChildActionCacheActive(filterContext))
throw new InvalidOperationException("You cannot use [AdminAuthorize] attribute when a child action cache is active");
if (IsAdminPageRequested(filterContext))
{
if (!this.HasAdminAccess(filterContext))
this.HandleUnauthorizedRequest(filterContext);
}
}
public virtual bool HasAdminAccess(AuthorizationContext filterContext)
{
var permissionService = EngineContext.Current.Resolve<IPermissionService>();
if (HttpContext.Current != null && HttpContext.Current.User != null)
{
var formsIdentity = HttpContext.Current.User.Identity as FormsIdentity;
if (formsIdentity == null)
{
return false;
}
var ticket = formsIdentity.Ticket;
if (ticket == null)
{
return false;
}
var usernameOrEmail = ticket.UserData;
if (!usernameOrEmail.Contains("_"))
{
return false;
}
if (HttpContext.Current.Session == null)
{
return false;
}
if (HttpContext.Current.Session["authID"] == null)
{
return false;
}
var authId = HttpContext.Current.Session["authID"].ToString();
var cookieAuthId = usernameOrEmail.Split('_')[1];
// Compare the random guids
if (!authId.Equals(cookieAuthId))
{
return false;
}
}
bool result = permissionService.Authorize(StandardPermissionProvider.AccessAdminPanel);
return result;
}
}