Skip to content
This repository has been archived by the owner on Oct 29, 2024. It is now read-only.

Commit

Permalink
Files without extension and with .html, .htm extensions are not allow…
Browse files Browse the repository at this point in the history
…ed to be uploaded by default. Added option to scan uploaded files via ClamAV (https://www.clamav.net/) or another via another scanner by implementing IUploadAVScanner interface. Made sure forgot password page don't reveal info about if an e-mail exists. Forgot password tokens can only be used once (they normally expire in 3 hours).
  • Loading branch information
volkanceylan committed Apr 6, 2023
1 parent ac5090b commit 6dce816
Show file tree
Hide file tree
Showing 20 changed files with 64 additions and 39 deletions.
6 changes: 4 additions & 2 deletions src/Serene.Web/Initialization/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void ConfigureServices(IServiceCollection services)
services.Configure<CssBundlingOptions>(Configuration.GetSection(CssBundlingOptions.SectionKey));
services.Configure<LocalTextPackages>(Configuration.GetSection(LocalTextPackages.SectionKey));
services.Configure<ScriptBundlingOptions>(Configuration.GetSection(ScriptBundlingOptions.SectionKey));
services.Configure<UploadSettings>(Configuration.GetSection(UploadSettings.SectionKey));
services.Configure<UploadSettings>(Configuration.GetSection(UploadSettings.SectionKey));
services.Configure<Serenity.Extensions.EnvironmentSettings>(
Configuration.GetSection(Serenity.Extensions.EnvironmentSettings.SectionKey));
{
Expand All @@ -72,6 +72,7 @@ public void ConfigureServices(IServiceCollection services)
});
}

services.ConfigureSection<Serenity.Extensions.ClamAVSettings>(Configuration);
services.Configure<Serenity.Extensions.EnvironmentSettings>(Configuration.GetSection(Serenity.Extensions.EnvironmentSettings.SectionKey));

services.Configure<RequestLocalizationOptions>(options =>
Expand Down Expand Up @@ -147,7 +148,8 @@ public void ConfigureServices(IServiceCollection services)
services.AddServiceHandlers();
services.AddDynamicScripts();
services.AddCssBundling();
services.AddScriptBundling();
services.AddScriptBundling();
services.AddSingleton<IUploadAVScanner, Serenity.Extensions.ClamAVUploadScanner>();
services.AddUploadStorage();
services.AddSingleton<Administration.IUserPasswordValidator, Administration.UserPasswordValidator>();
services.AddSingleton<IHttpContextItemsAccessor, HttpContextItemsAccessor>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using FluentMigrator;
using FluentMigrator;
using Serenity.Extensions;
using System;

Expand Down Expand Up @@ -28,7 +28,7 @@ public override void Up()
Insert.IntoTable("Users").Row(new {
Username = "admin",
DisplayName = "admin",
Email = "admin@dummy.com",
Email = "admin@domain" + Serenity.IO.TemporaryFileHelper.RandomFileCode() + ".com",
Source = "site",
PasswordHash = "rfqpSPYs0ekFlPyvIRTXsdhE/qrTHFF+kKsAUla7pFkXL4BgLGlTe89GDX5DBysenMDj8AqbIZPybqvusyCjwQ",
PasswordSalt = "hJf_F",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,34 @@ public ActionResult ForgotPassword()
return View(MVC.Views.Membership.Account.ForgotPassword.ForgotPasswordPage);
}

static int GetDeterministicHashCode(string str)
{
unchecked
{
int hash1 = (5381 << 16) + 5381;
int hash2 = hash1;

for (int i = 0; i < str.Length; i += 2)
{
hash1 = ((hash1 << 5) + hash1) ^ str[i];
if (i == str.Length - 1)
break;
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
}

return hash1 + (hash2 * 1566083941);
}
}

static int NonceForResetPassword(UserRow user)
{
return GetDeterministicHashCode(
(user.UpdateDate ?? user.InsertDate ?? DateTime.Today).ToString("s") +
user.PasswordHash + user.PasswordSalt);
}

[HttpPost, JsonRequest]
public Result<ServiceResponse> ForgotPassword(ForgotPasswordRequest request,
public Result<ServiceResponse> ForgotPassword(ForgotPasswordRequest request,
[FromServices] IEmailSender emailSender,
[FromServices] IOptions<EnvironmentSettings> options = null)
{
Expand All @@ -36,14 +62,15 @@ public Result<ServiceResponse> ForgotPassword(ForgotPasswordRequest request,

var user = connection.TryFirst<UserRow>(UserRow.Fields.Email == request.Email);
if (user == null)
throw new ValidationError("CantFindUserWithEmail", Texts.Validation.CantFindUserWithEmail.ToString(Localizer));
return new ServiceResponse();

byte[] bytes;
using (var ms = new MemoryStream())
using (var bw = new BinaryWriter(ms))
{
bw.Write(DateTime.UtcNow.AddHours(3).ToBinary());
bw.Write(user.UserId.Value);
bw.Write(NonceForResetPassword(user));
bw.Flush();
bytes = ms.ToArray();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public ActionResult ResetPassword(string t,
[FromServices] ISqlConnections sqlConnections)
{
int userId;
int nonce;
try
{
var bytes = HttpContext.RequestServices
Expand All @@ -30,6 +31,7 @@ public ActionResult ResetPassword(string t,
return Error(Texts.Validation.InvalidResetToken.ToString(Localizer));

userId = br.ReadInt32();
nonce = br.ReadInt32();
}
catch (Exception)
{
Expand All @@ -42,7 +44,7 @@ public ActionResult ResetPassword(string t,
using (var connection = sqlConnections.NewFor<UserRow>())
{
var user = connection.TryById<UserRow>(userId);
if (user == null)
if (user == null || nonce != NonceForResetPassword(user))
return Error(Texts.Validation.InvalidResetToken.ToString(Localizer));
}

Expand All @@ -67,6 +69,7 @@ public Result<ServiceResponse> ResetPassword(ResetPasswordRequest request,
.GetDataProtector("ResetPassword").Unprotect(Convert.FromBase64String(request.Token));

int userId;
int nonce;
using (var ms = new MemoryStream(bytes))
using (var br = new BinaryReader(ms))
{
Expand All @@ -75,6 +78,7 @@ public Result<ServiceResponse> ResetPassword(ResetPasswordRequest request,
throw new ValidationError(Texts.Validation.InvalidResetToken.ToString(Localizer));

userId = br.ReadInt32();
nonce = br.ReadInt32();
}

if (sqlConnections is null)
Expand All @@ -84,7 +88,7 @@ public Result<ServiceResponse> ResetPassword(ResetPasswordRequest request,
using (var connection = sqlConnections.NewFor<UserRow>())
{
user = connection.TryById<UserRow>(userId);
if (user == null)
if (user == null || nonce != NonceForResetPassword(user))
throw new ValidationError(Texts.Validation.InvalidResetToken.ToString(Localizer));
}

Expand All @@ -101,7 +105,8 @@ public Result<ServiceResponse> ResetPassword(ResetPasswordRequest request,
{
UserId = user.UserId.Value,
PasswordSalt = salt,
PasswordHash = hash
PasswordHash = hash,
UpdateDate = DateTime.Now
});

Cache.InvalidateOnCommit(uow, UserRow.Fields);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class ForgotPasswordPanel extends PropertyPanel<ForgotPasswordRequest, an
url: resolveUrl('~/Account/ForgotPassword'),
request: request,
onSuccess: response => {
information(Texts.Forms.Membership.ForgotPassword.Success, () => {
information(Texts.Forms.Membership.ForgotPassword.SuccessMessage, () => {
window.location.href = resolveUrl('~/');
});
}
Expand Down
7 changes: 3 additions & 4 deletions src/Serene.Web/Modules/ServerTypes/Texts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ namespace Serene.Texts {
export const FormInfo: string;
export const FormTitle: string;
export const SubmitButton: string;
export const Success: string;
export const SuccessMessage: string;
}

namespace Login {
Expand Down Expand Up @@ -216,7 +216,6 @@ namespace Serene.Texts {

export declare namespace Validation {
export const AuthenticationError: string;
export const CantFindUserWithEmail: string;
export const CurrentPasswordMismatch: string;
export const DeleteForeignKeyError: string;
export const EmailConfirm: string;
Expand All @@ -228,7 +227,7 @@ namespace Serene.Texts {
export const SavePrimaryKeyError: string;
}

Serene['Texts'] = proxyTexts(Texts, '', {Db:{Administration:{Language:{Id:1,LanguageId:1,LanguageName:1},Role:{RoleId:1,RoleName:1},RolePermission:{PermissionKey:1,RoleId:1,RolePermissionId:1,RoleRoleName:1},Translation:{CustomText:1,EntityPlural:1,Key:1,OverrideConfirmation:1,SaveChangesButton:1,SourceLanguage:1,SourceText:1,TargetLanguage:1,TargetText:1},User:{DisplayName:1,Email:1,InsertDate:1,InsertUserId:1,IsActive:1,LastDirectoryUpdate:1,Password:1,PasswordConfirm:1,PasswordHash:1,PasswordSalt:1,Roles:1,Source:1,UpdateDate:1,UpdateUserId:1,UserId:1,UserImage:1,Username:1},UserPermission:{Granted:1,PermissionKey:1,User:1,UserId:1,UserPermissionId:1,Username:1},UserRole:{RoleId:1,User:1,UserId:1,UserRoleId:1,Username:1}}},Forms:{Membership:{ChangePassword:{FormTitle:1,SubmitButton:1,Success:1},ForgotPassword:{BackToLogin:1,FormInfo:1,FormTitle:1,SubmitButton:1,Success:1},Login:{FacebookButton:1,ForgotPassword:1,GoogleButton:1,LoginToYourAccount:1,OR:1,RememberMe:1,SignInButton:1,#Button:1},ResetPassword:{BackToLogin:1,EmailSubject:1,FormTitle:1,SubmitButton:1,Success:1},#:{AcceptTerms:1,ActivateEmailSubject:1,ActivationCompleteMessage:1,BackToLogin:1,ConfirmEmail:1,ConfirmPassword:1,DisplayName:1,Email:1,FormInfo:1,FormTitle:1,Password:1,SubmitButton:1,Success:1}}},Navigation:{LogoutLink:1,SiteTitle:1},Site:{AccessDenied:{ClickToChangeUser:1,ClickToLogin:1,LackPermissions:1,NotLoggedIn:1,PageTitle:1},BasicProgressDialog:{CancelTitle:1,PleaseWait:1},BulkServiceAction:{AllHadErrorsFormat:1,AllSuccessFormat:1,ConfirmationFormat:1,ErrorCount:1,NothingToProcess:1,SomeHadErrorsFormat:1,SuccessCount:1},Dashboard:{ContentDescription:1},Layout:{FooterCopyright:1,FooterInfo:1,FooterRights:1,GeneralSettings:1,Language:1,Theme:1,ThemeBlack:1,ThemeBlackLight:1,ThemeBlue:1,ThemeBlueLight:1,ThemeGreen:1,ThemeGreenLight:1,ThemePurple:1,ThemePurpleLight:1,ThemeRed:1,ThemeRedLight:1,ThemeYellow:1,ThemeYellowLight:1},RolePermissionDialog:{DialogTitle:1,EditButton:1,SaveSuccess:1},UserDialog:{EditPermissionsButton:1,EditRolesButton:1},UserPermissionDialog:{DialogTitle:1,Grant:1,Permission:1,Revoke:1,SaveSuccess:1},UserRoleDialog:{DialogTitle:1,SaveSuccess:1},ValidationError:{Title:1}},Validation:{AuthenticationError:1,CantFindUserWithEmail:1,CurrentPasswordMismatch:1,DeleteForeignKeyError:1,EmailConfirm:1,EmailInUse:1,InvalidActivateToken:1,InvalidResetToken:1,MinRequiredPasswordLength:1,PasswordConfirmMismatch:1,SavePrimaryKeyError:1}}) as any;
Serene['Texts'] = proxyTexts(Texts, '', {Db:{Administration:{Language:{Id:1,LanguageId:1,LanguageName:1},Role:{RoleId:1,RoleName:1},RolePermission:{PermissionKey:1,RoleId:1,RolePermissionId:1,RoleRoleName:1},Translation:{CustomText:1,EntityPlural:1,Key:1,OverrideConfirmation:1,SaveChangesButton:1,SourceLanguage:1,SourceText:1,TargetLanguage:1,TargetText:1},User:{DisplayName:1,Email:1,InsertDate:1,InsertUserId:1,IsActive:1,LastDirectoryUpdate:1,Password:1,PasswordConfirm:1,PasswordHash:1,PasswordSalt:1,Roles:1,Source:1,UpdateDate:1,UpdateUserId:1,UserId:1,UserImage:1,Username:1},UserPermission:{Granted:1,PermissionKey:1,User:1,UserId:1,UserPermissionId:1,Username:1},UserRole:{RoleId:1,User:1,UserId:1,UserRoleId:1,Username:1}}},Forms:{Membership:{ChangePassword:{FormTitle:1,SubmitButton:1,Success:1},ForgotPassword:{BackToLogin:1,FormInfo:1,FormTitle:1,SubmitButton:1,SuccessMessage:1},Login:{FacebookButton:1,ForgotPassword:1,GoogleButton:1,LoginToYourAccount:1,OR:1,RememberMe:1,SignInButton:1,#Button:1},ResetPassword:{BackToLogin:1,EmailSubject:1,FormTitle:1,SubmitButton:1,Success:1},#:{AcceptTerms:1,ActivateEmailSubject:1,ActivationCompleteMessage:1,BackToLogin:1,ConfirmEmail:1,ConfirmPassword:1,DisplayName:1,Email:1,FormInfo:1,FormTitle:1,Password:1,SubmitButton:1,Success:1}}},Navigation:{LogoutLink:1,SiteTitle:1},Site:{AccessDenied:{ClickToChangeUser:1,ClickToLogin:1,LackPermissions:1,NotLoggedIn:1,PageTitle:1},BasicProgressDialog:{CancelTitle:1,PleaseWait:1},BulkServiceAction:{AllHadErrorsFormat:1,AllSuccessFormat:1,ConfirmationFormat:1,ErrorCount:1,NothingToProcess:1,SomeHadErrorsFormat:1,SuccessCount:1},Dashboard:{ContentDescription:1},Layout:{FooterCopyright:1,FooterInfo:1,FooterRights:1,GeneralSettings:1,Language:1,Theme:1,ThemeBlack:1,ThemeBlackLight:1,ThemeBlue:1,ThemeBlueLight:1,ThemeGreen:1,ThemeGreenLight:1,ThemePurple:1,ThemePurpleLight:1,ThemeRed:1,ThemeRedLight:1,ThemeYellow:1,ThemeYellowLight:1},RolePermissionDialog:{DialogTitle:1,EditButton:1,SaveSuccess:1},UserDialog:{EditPermissionsButton:1,EditRolesButton:1},UserPermissionDialog:{DialogTitle:1,Grant:1,Permission:1,Revoke:1,SaveSuccess:1},UserRoleDialog:{DialogTitle:1,SaveSuccess:1},ValidationError:{Title:1}},Validation:{AuthenticationError:1,CurrentPasswordMismatch:1,DeleteForeignKeyError:1,EmailConfirm:1,EmailInUse:1,InvalidActivateToken:1,InvalidResetToken:1,MinRequiredPasswordLength:1,PasswordConfirmMismatch:1,SavePrimaryKeyError:1}}) as any;
}

export const Texts = Serene.Texts;
export const Texts = Serene.Texts;
3 changes: 3 additions & 0 deletions src/Serene.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
"BackgroundJobs": {
"Enabled": true
},
"ClamAV": {
"Enabled": true
},
"EnvironmentSettings": {
"SiteExternalUrl": null
},
Expand Down
5 changes: 2 additions & 3 deletions src/Serene.Web/texts/Texts.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Serenity;
using Serenity;
using Serenity.ComponentModel;

namespace Serene
Expand Down Expand Up @@ -41,7 +41,7 @@ public static class ForgotPassword
public static LocalText FormInfo = "Please enter the e-mail you used to #.";
public static LocalText FormTitle = "Forgot My Password";
public static LocalText SubmitButton = "Reset My Password";
public static LocalText Success = "An e-mail with password reset instructions is sent to your e-mail address.";
public static LocalText SuccessMessage = "If this user exists, we have sent you an e-mail with password reset instructions.";
public static LocalText BackToLogin = "I remember my password";
}

Expand Down Expand Up @@ -194,7 +194,6 @@ public static partial class Validation
public static LocalText PasswordConfirmMismatch = "The passwords entered doesn't match!";
public static LocalText InvalidResetToken = "Your token to reset your password is invalid or has expired!";
public static LocalText InvalidActivateToken = "Your token to activate your account is invalid or has expired!";
public static LocalText CantFindUserWithEmail = "Can't find a user with that e-mail adress!";
public static LocalText EmailInUse = "Another user with this e-mail exists!";
public static LocalText EmailConfirm = "Emails entered doesn't match!";
public static LocalText DeleteForeignKeyError = "Can't delete record. '{0}' table has " +
Expand Down
3 changes: 1 addition & 2 deletions src/Serene.Web/texts/resources/site.texts.de.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"Forms.Membership.ForgotPassword.FormInfo": "Bitte geben Sie die E-Mail-Adresse ein, die Sie zur Anmeldung verwendet haben",
"Forms.Membership.ForgotPassword.FormTitle": "Ich habe mein Passwort vergessen",
"Forms.Membership.ForgotPassword.SubmitButton": "Passwort zurücksetzen",
"Forms.Membership.ForgotPassword.Success": "Eine E-Mail mit Anweisungen zum Zurücksetzen des Passworts wurde an Ihre E-Mail-Adresse gesendet",
"Forms.Membership.ForgotPassword.SuccessMessage": "Wenn dieser Benutzer existiert, haben wir Ihnen eine E-Mail mit Anweisungen zum Zurücksetzen des Passworts gesendet.",
"Forms.Membership.Login.ForgotPassword": "Passwort vergessen?",
"Forms.Membership.Login.LoginToYourAccount": "Melden Sie sich bei Ihrem Konto an",
"Forms.Membership.Login.Password_Placeholder": "Passwort",
Expand Down Expand Up @@ -133,7 +133,6 @@
"Site.UserPermissionDialog.SaveSuccess": "Benutzerberechtigungen aktualisiert",
"Site.ValidationError.Title": "FEHLER",
"Validation.AuthenticationError": "Validierungsfehler: Ungültiger Benutzername oder Passwort!",
"Validation.CantFindUserWithEmail": "Kann keinen Benutzer mit dieser E-Mail-Adresse finden!",
"Validation.CurrentPasswordMismatch": "Ihr aktuelles Passwort ist nicht gültig!",
"Validation.DeleteForeignKeyError": null,
"Validation.EmailConfirm": "Die eingegebenen E-Mails stimmen nicht überein!",
Expand Down
3 changes: 1 addition & 2 deletions src/Serene.Web/texts/resources/site.texts.es.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"Forms.Membership.ForgotPassword.FormInfo": "Por favor, introduzca el correo electrónico que utilizó para registrarse",
"Forms.Membership.ForgotPassword.FormTitle": "He olvidado mi contraseña",
"Forms.Membership.ForgotPassword.SubmitButton": "Restablecer mi contraseña",
"Forms.Membership.ForgotPassword.Success": "Se ha enviado un correo electrónico con instrucciones para restablecer la contraseña a su dirección de correo electrónico",
"Forms.Membership.ForgotPassword.SuccessMessage": "Si este usuario existe, le hemos enviado un correo electrónico con instrucciones para restablecer la contraseña.",
"Forms.Membership.Login.ForgotPassword": "¿Olvidaste tu contraseña?",
"Forms.Membership.Login.LoginToYourAccount": "Iniciar sesión en su cuenta",
"Forms.Membership.Login.Password_Placeholder": "contraseña",
Expand Down Expand Up @@ -133,7 +133,6 @@
"Site.UserPermissionDialog.SaveSuccess": "Permisos de usuario actualizados",
"Site.ValidationError.Title": "ERROR",
"Validation.AuthenticationError": "Error de validación: ¡Nombre de usuario o contraseña inválidos!",
"Validation.CantFindUserWithEmail": "¡No se puede encontrar un usuario con esa dirección de correo electrónico!",
"Validation.CurrentPasswordMismatch": "¡Tu contraseña actual no es válida!",
"Validation.DeleteForeignKeyError": null,
"Validation.EmailConfirm": "¡Los correos electrónicos ingresados no coinciden!",
Expand Down
Loading

0 comments on commit 6dce816

Please # to comment.