diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fd612f..1811e1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## [3.2.0] - 2023-01-03 +### Added +- Support for credentials intelligence protocols `v2` and `multistep_sso` +- Support for login successful reporting methods `header`, `status`, `body`, and `custom` +- Support for automatic sending of `additional_s2s` activity +- Support for manual sending of `additional_s2s` activity via header +- Support for sending raw username on `additional_s2s` activity +- Support for login credentials extraction via custom function +- New `request_id` field to all enforcer activities + ## [3.1.4] - 2022-12-05 ### Added - Compatibility with .NET Framework 4.7 and higher diff --git a/PerimeterXModule/CustomBehavior/ICredentialsExtractionHandler.cs b/PerimeterXModule/CustomBehavior/ICredentialsExtractionHandler.cs new file mode 100644 index 0000000..8f91808 --- /dev/null +++ b/PerimeterXModule/CustomBehavior/ICredentialsExtractionHandler.cs @@ -0,0 +1,9 @@ +using System.Web; + +namespace PerimeterX.CustomBehavior +{ + public interface ICredentialsExtractionHandler + { + ExtractedCredentials Handle(HttpRequest httpRequest); + } +} diff --git a/PerimeterXModule/CustomBehavior/ILoginSuccessfulHandler.cs b/PerimeterXModule/CustomBehavior/ILoginSuccessfulHandler.cs new file mode 100644 index 0000000..46a4dcf --- /dev/null +++ b/PerimeterXModule/CustomBehavior/ILoginSuccessfulHandler.cs @@ -0,0 +1,9 @@ +using System.Web; + +namespace PerimeterX.CustomBehavior +{ + public interface ILoginSuccessfulHandler + { + bool Handle(HttpResponse httpResponse); + } +} diff --git a/PerimeterXModule/DataContracts/Activities/ActivityDetails.cs b/PerimeterXModule/DataContracts/Activities/ActivityDetails.cs index 3721266..b0dbb77 100644 --- a/PerimeterXModule/DataContracts/Activities/ActivityDetails.cs +++ b/PerimeterXModule/DataContracts/Activities/ActivityDetails.cs @@ -40,6 +40,18 @@ public class ActivityDetails : IActivityDetails [DataMember(Name = "simulated_block")] public bool? SimulatedBlock; + + [DataMember(Name = "ci_version")] + public string CiVersion { get; set; } + + [DataMember(Name = "credentials_compromised")] + public bool CredentialsCompromised { get; set; } + + [DataMember(Name = "sso_step")] + public string SsoStep { get; set; } + + [DataMember(Name = "request_id")] + public string RequestId { get; set; } } [DataContract] @@ -59,5 +71,37 @@ public class EnforcerTelemetryActivityDetails : IActivityDetails [DataMember(Name = "enforcer_configs")] public string EnforcerConfigs; - } + } + + [DataContract] + public class AdditionalS2SActivityDetails : IActivityDetails + { + + [DataMember(Name = "client_uuid")] + public string ClientUuid { get; set; } + + [DataMember(Name = "request_id")] + public string RequestId { get; set; } + + [DataMember(Name = "ci_version")] + public string CiVersion { get; set; } + + [DataMember(Name = "credentials_compromised", EmitDefaultValue = false)] + public bool CredentialsCompromised { get; set; } + + [DataMember(Name = "http_status_code")] + public int HttpStatusCode { get; set; } + + [DataMember(Name = "login_successful")] + public bool LoginSuccessful { get; set; } + + [DataMember(Name = "raw_username")] + public string RawUsername { get; set; } + + [DataMember(Name = "sso_step")] + public string SsoStep { get; set; } + + [DataMember(Name = "module_version")] + public string ModuleVersion { get; internal set; } + } } diff --git a/PerimeterXModule/DataContracts/Requests/Additional.cs b/PerimeterXModule/DataContracts/Requests/Additional.cs index 8d6ee74..6f8767c 100644 --- a/PerimeterXModule/DataContracts/Requests/Additional.cs +++ b/PerimeterXModule/DataContracts/Requests/Additional.cs @@ -46,5 +46,24 @@ public class Additional [DataMember(Name = "enforcer_vid_source", EmitDefaultValue = false)] public string VidSource; - } + + [DataMember(Name = "user")] + public string Username; + + [DataMember(Name = "pass")] + public string Password; + + [DataMember(Name = "raw_username", IsRequired = false)] + public string RawUsername; + + [DataMember(Name = "ci_version")] + public string CiVersion; + + [DataMember(Name = "sso_step", IsRequired = false)] + public string SsoStep; + + [DataMember(Name = "request_id")] + public string RequestId { get; set; } + + } } diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/CredentialIntelligenceManager.cs b/PerimeterXModule/Internals/CredentialsIntelligence/CredentialIntelligenceManager.cs new file mode 100644 index 0000000..3cd0acc --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/CredentialIntelligenceManager.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Web; +using Jil; +using PerimeterX.CustomBehavior; + +namespace PerimeterX +{ + public class CredentialIntelligenceManager + { + private ICredentialsIntelligenceProtocol protocol; + private List loginCredentialsExtractors; + + public CredentialIntelligenceManager(string ciVersion, List loginCredentialsExraction) + { + this.protocol = CredentialsIntelligenceProtocolFactory.Create(ciVersion); + this.loginCredentialsExtractors = loginCredentialsExraction; + } + + public LoginCredentialsFields ExtractCredentialsFromRequest(PxContext context, HttpRequest request, ICredentialsExtractionHandler credentialsExtractionHandler) + { + try + { + ExtractorObject extractionDetails = FindMatchCredentialsDetails(request); + + if (extractionDetails != null) + { + ExtractedCredentials extractedCredentials = ExtractLoginCredentials(context, request, credentialsExtractionHandler, extractionDetails); + + + if (extractedCredentials != null) + { + return protocol.ProcessCredentials(extractedCredentials); + } + } + + } catch (Exception ex) + { + PxLoggingUtils.LogError(string.Format("Failed to extract credentials.", ex.Message)); + } + + return null; + } + + private ExtractedCredentials ExtractLoginCredentials(PxContext context, HttpRequest request, ICredentialsExtractionHandler credentialsExtractionHandler, ExtractorObject extractionDetails) + { + if (credentialsExtractionHandler != null) + { + return credentialsExtractionHandler.Handle(request); + } + else + { + return HandleExtractCredentials(extractionDetails, context, request); + } + } + + private ExtractorObject FindMatchCredentialsDetails(HttpRequest request) + { + foreach (ExtractorObject loginObject in this.loginCredentialsExtractors) + { + if (IsRequestMatchLoginRequestConfiguration(loginObject, request)) + { + return loginObject; + } + } + + return null; + } + + private static bool IsRequestMatchLoginRequestConfiguration(ExtractorObject extractorObject, HttpRequest request) + { + if (request.HttpMethod.ToLower() == extractorObject.Method) + { + if (extractorObject.PathType == "exact" && request.Path == extractorObject.Path) + { + return true; + } + + if (extractorObject.PathType == "regex" && Regex.IsMatch(request.Path, extractorObject.Path)) + { + return true; + } + } + + return false; + } + + private ExtractedCredentials HandleExtractCredentials(ExtractorObject extractionDetails, PxContext pxContext, HttpRequest request) + { + string userFieldName = extractionDetails.UserFieldName; + string passwordFieldName = extractionDetails.PassFieldName; + + if (userFieldName == null || passwordFieldName == null) + { + return null; + } + + Dictionary headers = pxContext.GetLowercaseHeadersAsDictionary(); + + if (extractionDetails.SentThrough == "header") + { + return ExtractFromHeader(userFieldName, passwordFieldName, headers); + } else if (extractionDetails.SentThrough == "query-param") + { + return new ExtractedCredentials( + request.QueryString[userFieldName].Replace(" ", "+"), + request.QueryString[passwordFieldName].Replace(" ", "+") + ); + } else if (extractionDetails.SentThrough == "body") + { + return ExtractFromBody(userFieldName, passwordFieldName, headers, request); + } + + return null; + } + + public static ExtractedCredentials ExtractFromHeader(string userFieldName, string passwordFieldName, Dictionary headers) + { + bool isUsernameHeaderExist = headers.TryGetValue(userFieldName.ToLower(), out string userName); + bool isPasswordHeaderExist = headers.TryGetValue(passwordFieldName.ToLower(), out string password); + + if (!isUsernameHeaderExist && !isPasswordHeaderExist) { return null; } + + return new ExtractedCredentials(userName, password); + } + + private ExtractedCredentials ExtractFromBody(string userFieldName, string passwordFieldName, Dictionary headers, HttpRequest request) + { + bool isContentTypeHeaderExist = headers.TryGetValue("content-type", out string contentType); + + string body = BodyReader.ReadRequestBody(request); + + if (!isContentTypeHeaderExist) + { + return null; + } else if (contentType.Contains("application/json")) + { + return ExtractCredentialsFromJson(body, userFieldName, passwordFieldName); + } else if (contentType.Contains("x-www-form-urlencoded")) + { + return ReadValueFromUrlEncoded(body, userFieldName, passwordFieldName); + } else if (contentType.Contains("form-data")) + { + return ExtractValueFromMultipart(body, contentType, userFieldName, passwordFieldName); + } + + return null; + } + + private ExtractedCredentials ExtractCredentialsFromJson(string body, string userFieldName, string passwordFieldName) { + + dynamic jsonBody = JSON.DeserializeDynamic(body, PxConstants.JSON_OPTIONS); + + string userValue = PxCommonUtils.ExtractValueFromNestedJson(userFieldName, jsonBody); + string passValue = PxCommonUtils.ExtractValueFromNestedJson(passwordFieldName, jsonBody); + + return new ExtractedCredentials(userValue, passValue); + } + + private ExtractedCredentials ReadValueFromUrlEncoded(string body, string userFieldName, string passwordFieldName) + { + var parametersQueryString = HttpUtility.ParseQueryString(body); + var parametersDictionary = new Dictionary(); + foreach (var key in parametersQueryString.AllKeys) + { + parametersDictionary.Add(key, parametersQueryString[key]); + } + + return ExtractCredentialsFromDictionary(parametersDictionary, userFieldName, passwordFieldName); + } + + private ExtractedCredentials ExtractValueFromMultipart(string body, string contentType, string userFieldName, string passwordFieldName) + { + Dictionary formData = BodyReader.GetFormDataContentAsDictionary(body, contentType); + + return ExtractCredentialsFromDictionary(formData, userFieldName, passwordFieldName); + } + + private ExtractedCredentials ExtractCredentialsFromDictionary(Dictionary parametersDictionary, string userFieldName, string passwordFieldName) + { + bool isUsernameExist = parametersDictionary.TryGetValue(userFieldName, out string userField); + bool isPasswordExist = parametersDictionary.TryGetValue(passwordFieldName, out string passwordField); + + if (!isUsernameExist && !isPasswordExist) + { + return null; + } + + return new ExtractedCredentials(userField, passwordField); + } + } + + +} diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/CredentialsIntelligenceProtocolFactory.cs b/PerimeterXModule/Internals/CredentialsIntelligence/CredentialsIntelligenceProtocolFactory.cs new file mode 100644 index 0000000..67c9f9c --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/CredentialsIntelligenceProtocolFactory.cs @@ -0,0 +1,21 @@ +using System; +using PerimeterX; + +namespace PerimeterX +{ + public class CredentialsIntelligenceProtocolFactory + { + public static ICredentialsIntelligenceProtocol Create(string protocolVersion) + { + switch(protocolVersion) + { + case CIVersion.V2: + return new V2CredentialsIntelligenceProtocol(); + case CIVersion.MULTISTEP_SSO: + return new MultistepSSoCredentialsIntelligenceProtocol(); + default: + throw new Exception("Unknown CI protocol version: " + protocolVersion); + } + } + } +} diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/ExtractedCredentials.cs b/PerimeterXModule/Internals/CredentialsIntelligence/ExtractedCredentials.cs new file mode 100644 index 0000000..390bd01 --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/ExtractedCredentials.cs @@ -0,0 +1,19 @@ +using System.Runtime.Serialization; + +namespace PerimeterX +{ + public class ExtractedCredentials + { + [DataMember(Name = "username")] + public string Username { get; set; } + + [DataMember(Name = "password")] + public string Password { get; set; } + + public ExtractedCredentials(string username, string password) + { + this.Username = username; + this.Password = password; + } + } +} diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/ExtractorObject.cs b/PerimeterXModule/Internals/CredentialsIntelligence/ExtractorObject.cs new file mode 100644 index 0000000..ecf167a --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/ExtractorObject.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; + +namespace PerimeterX +{ + [DataContract] + public class ExtractorObject + { + [DataMember(Name = "path")] + public string Path; + + [DataMember(Name = "path_type")] + public string PathType; + + [DataMember(Name = "method")] + public string Method; + + [DataMember(Name = "sent_through")] + public string SentThrough; + + [DataMember(Name = "pass_field")] + public string PassFieldName; + + [DataMember(Name = "user_field")] + public string UserFieldName; + } +} diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/ICredentialsIntelligenceProtocol.cs b/PerimeterXModule/Internals/CredentialsIntelligence/ICredentialsIntelligenceProtocol.cs new file mode 100644 index 0000000..4308652 --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/ICredentialsIntelligenceProtocol.cs @@ -0,0 +1,8 @@ + +namespace PerimeterX +{ + public interface ICredentialsIntelligenceProtocol + { + LoginCredentialsFields ProcessCredentials(ExtractedCredentials extractedCredentials); + } +} diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/LoginCredentialsFields.cs b/PerimeterXModule/Internals/CredentialsIntelligence/LoginCredentialsFields.cs new file mode 100644 index 0000000..eec0d9b --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/LoginCredentialsFields.cs @@ -0,0 +1,39 @@ +using System.Runtime.Serialization; + +namespace PerimeterX +{ + public class LoginCredentialsFields + { + [DataMember(Name = "username")] + public string Username; + + [DataMember(Name = "password")] + public string Password; + + [DataMember(Name = "rawUsername", IsRequired = false)] + public string RawUsername; + + [DataMember(Name = "version")] + public string CiVersion; + + [DataMember(Name = "ssoStep", IsRequired = false)] + public string SsoStep; + + public LoginCredentialsFields(string username, string password, string rawUsername, string ciVersion, string SsoStep) + { + this.Username = username; + this.Password = password; + this.RawUsername = rawUsername; + this.CiVersion = ciVersion; + this.SsoStep = SsoStep; + } + + public LoginCredentialsFields(string username, string password, string rawUsername, string version) + { + this.Username = username; + this.Password = password; + this.RawUsername = rawUsername; + this.CiVersion = version; + } + } +} diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/AdditionalS2SUtils.cs b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/AdditionalS2SUtils.cs new file mode 100644 index 0000000..861dce1 --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/AdditionalS2SUtils.cs @@ -0,0 +1,54 @@ +namespace PerimeterX +{ + public class AdditionalS2SUtils + { + public static Activity CreateAdditionalS2SActivity(PxModuleConfigurationSection config, int? statusCode, bool? loginSuccessful, PxContext pxContext) + { + bool isBreachedAccount = pxContext.IsBreachedAccount(); + LoginCredentialsFields loginCredentialsFields = pxContext.LoginCredentialsFields; + + bool shouldAddRawUsername = isBreachedAccount && + config.SendRawUsernameOnAdditionalS2SActivity && + loginCredentialsFields.RawUsername != null; + + AdditionalS2SActivityDetails details = new AdditionalS2SActivityDetails + { + ModuleVersion = PxConstants.MODULE_VERSION, + ClientUuid = pxContext.UUID, + RequestId = pxContext.RequestId, + CiVersion = config.CiVersion, + CredentialsCompromised = isBreachedAccount, + SsoStep = config.CiVersion == CIVersion.MULTISTEP_SSO ? loginCredentialsFields.SsoStep : null + }; + + if (statusCode.HasValue) + { + details.HttpStatusCode = statusCode.Value; + } + + if (loginSuccessful.HasValue) + { + bool isLoginSuccessful = loginSuccessful.Value; + details.LoginSuccessful = isLoginSuccessful; + details.RawUsername = shouldAddRawUsername && isLoginSuccessful ? loginCredentialsFields.RawUsername : null; + } + + var activity = new Activity + { + Type = "additional_s2s", + Timestamp = PxConstants.GetTimestamp(), + AppId = config.AppId, + SocketIP = pxContext.Ip, + Url = pxContext.FullUrl, + Details = details, + }; + + if (!string.IsNullOrEmpty(pxContext.Vid)) + { + activity.Vid = pxContext.Vid; + } + + return activity; + } + } +} diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/BodyLoginSuccessfulParser.cs b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/BodyLoginSuccessfulParser.cs new file mode 100644 index 0000000..db8a1d8 --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/BodyLoginSuccessfulParser.cs @@ -0,0 +1,25 @@ +using System.Web; +using System.Text.RegularExpressions; + +namespace PerimeterX +{ + public class BodyLoginSuccessfulParser : ILoginSuccessfulParser + { + private readonly string bodyRegex; + + public BodyLoginSuccessfulParser(PxModuleConfigurationSection config) { + bodyRegex = config.LoginSuccessfulBodyRegex; + } + + public bool? IsLoginSuccessful(HttpResponse httpResponse) + { + string body = ((OutputFilterStream)httpResponse.Filter).ReadStream(); + + if (body == null) { + return null; + } + + return Regex.IsMatch(body, bodyRegex); + } + } +} \ No newline at end of file diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/CustomLoginSuccessfulParser.cs b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/CustomLoginSuccessfulParser.cs new file mode 100644 index 0000000..9f5868d --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/CustomLoginSuccessfulParser.cs @@ -0,0 +1,34 @@ +using System; +using System.Web; +using PerimeterX.CustomBehavior; + +namespace PerimeterX +{ + public class CustomLoginSuccessfulParser : ILoginSuccessfulParser + { + ILoginSuccessfulHandler loginSuccessfulParserHandler; + + public CustomLoginSuccessfulParser(PxModuleConfigurationSection config) + { + loginSuccessfulParserHandler = PxCustomFunctions.GetCustomLoginSuccessfulHandler(config.CustomLoginSuccessfulHandler); + } + + public bool? IsLoginSuccessful(HttpResponse httpResponse) + { + try + { + if (loginSuccessfulParserHandler != null) + { + return loginSuccessfulParserHandler.Handle(httpResponse); + + } + } + catch (Exception ex) + { + PxLoggingUtils.LogDebug("An error occurred while executing login successful handler " + ex.Message); + } + + return null; + } + } +} \ No newline at end of file diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/HeaderLoginSuccessfulParser.cs b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/HeaderLoginSuccessfulParser.cs new file mode 100644 index 0000000..9f779a4 --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/HeaderLoginSuccessfulParser.cs @@ -0,0 +1,21 @@ +using System.Web; + +namespace PerimeterX +{ + public class HeaderLoginSuccessfulParser : ILoginSuccessfulParser + { + private readonly string successfulHeaderName; + private readonly string successfulHeaderValue; + + public HeaderLoginSuccessfulParser(PxModuleConfigurationSection config) + { + successfulHeaderName = config.LoginSuccessfulHeaderName; + successfulHeaderValue = config.LoginSuccessfulHeaderValue; + } + + public bool? IsLoginSuccessful(HttpResponse httpResponse) + { + return httpResponse.Headers[successfulHeaderName] == successfulHeaderValue; + } + } +} diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/ILoginSuccessfulParser.cs b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/ILoginSuccessfulParser.cs new file mode 100644 index 0000000..990f6a3 --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/ILoginSuccessfulParser.cs @@ -0,0 +1,9 @@ +using System.Web; + +namespace PerimeterX +{ + public interface ILoginSuccessfulParser + { + bool? IsLoginSuccessful(HttpResponse httpResponse); + } +} \ No newline at end of file diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/LoginSuccessfulParserFactory.cs b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/LoginSuccessfulParserFactory.cs new file mode 100644 index 0000000..bd3fcf3 --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/LoginSuccessfulParserFactory.cs @@ -0,0 +1,23 @@ + +namespace PerimeterX +{ + public class LoginSuccessfulParserFactory + { + public static ILoginSuccessfulParser Create(PxModuleConfigurationSection config) + { + switch (config.LoginSuccessfulReportingMethod) + { + case ("body"): + return new BodyLoginSuccessfulParser(config); + case ("header"): + return new HeaderLoginSuccessfulParser(config); + case ("status"): + return new StatusLoginSuccessfulParser(config); + case ("custom"): + return new CustomLoginSuccessfulParser(config); + default: + return null; + } + } + } +} diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/StatusLoginSuccessfulParser.cs b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/StatusLoginSuccessfulParser.cs new file mode 100644 index 0000000..7de55a0 --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/LoginSuccessful/StatusLoginSuccessfulParser.cs @@ -0,0 +1,21 @@ +using System.Linq; +using System.Web; +using System.Collections.Generic; + +namespace PerimeterX +{ + public class StatusLoginSuccessfulParser : ILoginSuccessfulParser + { + private readonly List successfulStatuses; + + public StatusLoginSuccessfulParser(PxModuleConfigurationSection config) + { + successfulStatuses = config.LoginSuccessfulStatus.Cast().ToList(); + } + + public bool? IsLoginSuccessful(HttpResponse httpResponse) + { + return successfulStatuses.Contains(httpResponse.StatusCode.ToString()); + } + } +} \ No newline at end of file diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/MultistepSSOCredentialsIntelligenceProtocol.cs b/PerimeterXModule/Internals/CredentialsIntelligence/MultistepSSOCredentialsIntelligenceProtocol.cs new file mode 100644 index 0000000..ce7acdf --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/MultistepSSOCredentialsIntelligenceProtocol.cs @@ -0,0 +1,36 @@ +namespace PerimeterX +{ + public class MultistepSSoCredentialsIntelligenceProtocol : ICredentialsIntelligenceProtocol + { + public LoginCredentialsFields ProcessCredentials(ExtractedCredentials extractedCredentials) + { + string rawUsername = null; + string password = null; + string ssoStep = MultistepSsoStep.PASSWORD; + + if (extractedCredentials.Username != null && extractedCredentials.Username.Length > 0) + { + rawUsername = extractedCredentials.Username; + ssoStep = MultistepSsoStep.USER; + } + + if (extractedCredentials.Password != null && extractedCredentials.Password.Length > 0) + { + password = PxCommonUtils.Sha256(extractedCredentials.Password); + } + + if (rawUsername == null && password == null) + { + return null; + } + + return new LoginCredentialsFields( + rawUsername, + password, + rawUsername, + CIVersion.MULTISTEP_SSO, + ssoStep + ); + } + } +} diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/OutputFilterStream.cs b/PerimeterXModule/Internals/CredentialsIntelligence/OutputFilterStream.cs new file mode 100644 index 0000000..8079d9c --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/OutputFilterStream.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PerimeterX +{ + /// + /// A stream which keeps an in-memory copy as it passes the bytes through + /// + public class OutputFilterStream : Stream + { + private readonly Stream InnerStream; + private readonly MemoryStream CopyStream; + + public OutputFilterStream(Stream inner) + { + this.InnerStream = inner; + this.CopyStream = new MemoryStream(); + } + + public string ReadStream() + { + lock (this.InnerStream) + { + if (this.CopyStream.Length <= 0L || + !this.CopyStream.CanRead || + !this.CopyStream.CanSeek) + { + return String.Empty; + } + + long pos = this.CopyStream.Position; + this.CopyStream.Position = 0L; + try + { + return new StreamReader(this.CopyStream).ReadToEnd(); + } + finally + { + try + { + this.CopyStream.Position = pos; + } + catch { } + } + } + } + + + public override bool CanRead + { + get { return this.InnerStream.CanRead; } + } + + public override bool CanSeek + { + get { return this.InnerStream.CanSeek; } + } + + public override bool CanWrite + { + get { return this.InnerStream.CanWrite; } + } + + public override void Flush() + { + this.InnerStream.Flush(); + } + + public override long Length + { + get { return this.InnerStream.Length; } + } + + public override long Position + { + get { return this.InnerStream.Position; } + set { this.CopyStream.Position = this.InnerStream.Position = value; } + } + + public override int Read(byte[] buffer, int offset, int count) + { + return this.InnerStream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + this.CopyStream.Seek(offset, origin); + return this.InnerStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + this.CopyStream.SetLength(value); + this.InnerStream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + this.CopyStream.Write(buffer, offset, count); + this.InnerStream.Write(buffer, offset, count); + } + } +} diff --git a/PerimeterXModule/Internals/CredentialsIntelligence/V2CredentialsIntelligenceProtocol.cs b/PerimeterXModule/Internals/CredentialsIntelligence/V2CredentialsIntelligenceProtocol.cs new file mode 100644 index 0000000..755be61 --- /dev/null +++ b/PerimeterXModule/Internals/CredentialsIntelligence/V2CredentialsIntelligenceProtocol.cs @@ -0,0 +1,57 @@ +namespace PerimeterX +{ + public class V2CredentialsIntelligenceProtocol : ICredentialsIntelligenceProtocol + { + public LoginCredentialsFields ProcessCredentials(ExtractedCredentials extractedCredentials) + { + + string username = extractedCredentials.Username; + string password = extractedCredentials.Password; + if (username == null || password == null || username.Length == 0 || password.Length == 0) + { + return null; + } + + string normalizedUsername = PxCommonUtils.IsEmailAddress(username) ? NormalizeEmailAddress(username) : username; + string hashedUsername = PxCommonUtils.Sha256(normalizedUsername); + string hashedPassword = HashPassword(hashedUsername, password); + + return new LoginCredentialsFields( + hashedUsername, + hashedPassword, + username, + CIVersion.V2 + ); + } + + public static string NormalizeEmailAddress(string emailAddress) + { + string lowercaseEmail = emailAddress.Trim().ToLower(); + int atIndex = lowercaseEmail.IndexOf("@"); + string normalizedUsername = lowercaseEmail.Substring(0, atIndex); + int plusIndex = normalizedUsername.IndexOf("+"); + normalizedUsername.Replace("+", ""); + + if (plusIndex > -1) + { + normalizedUsername = normalizedUsername.Substring(0, plusIndex); + } + + string domain = lowercaseEmail.Substring(atIndex); + + if (domain == "@gmail.com") + { + normalizedUsername = normalizedUsername.Replace(".", ""); + } + + return normalizedUsername + domain; + } + + public static string HashPassword(string salt, string password) + { + string hashedPassword = PxCommonUtils.Sha256(password); + return PxCommonUtils.Sha256(salt + hashedPassword); + } + + } +} diff --git a/PerimeterXModule/Internals/Enums/CIVersion.cs b/PerimeterXModule/Internals/Enums/CIVersion.cs new file mode 100644 index 0000000..117fa70 --- /dev/null +++ b/PerimeterXModule/Internals/Enums/CIVersion.cs @@ -0,0 +1,8 @@ +namespace PerimeterX +{ + public class CIVersion + { + public const string MULTISTEP_SSO = "multistep_sso"; + public const string V2 = "v2"; + } +} diff --git a/PerimeterXModule/Internals/Enums/MultistepSsoStep.cs b/PerimeterXModule/Internals/Enums/MultistepSsoStep.cs new file mode 100644 index 0000000..d6d7af8 --- /dev/null +++ b/PerimeterXModule/Internals/Enums/MultistepSsoStep.cs @@ -0,0 +1,8 @@ +namespace PerimeterX +{ + public class MultistepSsoStep + { + public const string USER = "user"; + public const string PASSWORD = "pass"; + } +} diff --git a/PerimeterXModule/Internals/Helpers/BodyReader.cs b/PerimeterXModule/Internals/Helpers/BodyReader.cs new file mode 100644 index 0000000..1f4c420 --- /dev/null +++ b/PerimeterXModule/Internals/Helpers/BodyReader.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System; +using System.IO; +using System.Linq; +using System.Runtime.Remoting.Contexts; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web; + +namespace PerimeterX +{ + public class BodyReader + { + public static string ReadRequestBody(HttpRequest request) + { + MemoryStream memstream = new MemoryStream(); + request.InputStream.CopyTo(memstream); + memstream.Position = 0; + using (StreamReader reader = new StreamReader(memstream)) + { + string text = reader.ReadToEnd(); + request.InputStream.Position = 0; + return text; + } + } + + public static Dictionary GetFormDataContentAsDictionary(string body, string contentType) + { + var formData = new Dictionary(); + + var boundary = contentType.Split(';') + .SingleOrDefault(x => x.Trim().StartsWith("boundary="))? + .Split('=')[1]; + + var parts = body.Split(new[] { "--" + boundary }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var part in parts) + { + var lines = part.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); + + if (part.StartsWith("--")) + { + continue; + } + + var key = string.Empty; + var value = new StringBuilder(); + foreach (var line in lines) + { + if (line.StartsWith("Content-Disposition")) + { + key = line.Split(new[] { "name=" }, StringSplitOptions.RemoveEmptyEntries)[1].Trim('\"'); + } + else + { + value.Append(line); + } + } + + formData.Add(key, value.ToString()); + } + + return formData; + } + } +} \ No newline at end of file diff --git a/PerimeterXModule/Internals/Helpers/PxCommonUtils.cs b/PerimeterXModule/Internals/Helpers/PxCommonUtils.cs index 4eb97e1..b1d9e2c 100644 --- a/PerimeterXModule/Internals/Helpers/PxCommonUtils.cs +++ b/PerimeterXModule/Internals/Helpers/PxCommonUtils.cs @@ -2,13 +2,16 @@ using System.Collections; using System.Net; using System.Reflection; +using System.Text.RegularExpressions; using System.Web; +using System.Security.Cryptography; +using System.Text; namespace PerimeterX { class PxCommonUtils { - /** + /** * * Request helper, extracting the ip from the request according to socketIpHeader or from * the request socket when socketIpHeader is absent @@ -17,7 +20,7 @@ class PxCommonUtils * PxConfiguration * Ip from the request */ - public static string GetRequestIP(HttpContext context, PxModuleConfigurationSection pxConfig) + public static string GetRequestIP(HttpContext context, PxModuleConfigurationSection pxConfig) { // Get IP from custom header string socketIpHeader = pxConfig.SocketIpHeader; @@ -68,5 +71,43 @@ public static void AddHeaderToRequest(HttpContext context, string key, string va ro.SetValue(headers, true, null); } } + + public static bool IsEmailAddress(string str) + { + return Regex.IsMatch(str, PxConstants.EMAIL_ADDRESS_REGEX, RegexOptions.IgnoreCase); + } + + public static string Sha256(string str) + { + using (SHA256 sha256 = SHA256.Create()) + { + byte[] inputBytes = Encoding.UTF8.GetBytes(str); + byte[] hash = sha256.ComputeHash(inputBytes); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + + public static string ExtractValueFromNestedJson(string pathToValue, dynamic jsonObject, char seperatorChar = '.') + { + + string[] stepsToCredentialInBody = pathToValue.Split(seperatorChar); + + dynamic result = jsonObject; + + foreach (string step in stepsToCredentialInBody) + { + if (result == null || !result.ContainsKey(step)) + { + result = null; + break; + } + else + { + result = result[step]; + } + } + + return result; + } } } diff --git a/PerimeterXModule/Internals/Helpers/PxConstants.cs b/PerimeterXModule/Internals/Helpers/PxConstants.cs index 6e5200c..94626c3 100644 --- a/PerimeterXModule/Internals/Helpers/PxConstants.cs +++ b/PerimeterXModule/Internals/Helpers/PxConstants.cs @@ -35,9 +35,9 @@ public static class PxConstants public static readonly string FIRST_PARTY_VALUE = "1"; public static readonly string COOKIE_HEADER = "cookie"; public static readonly string ENFORCER_TELEMETRY_HEADER = "X-PX-ENFORCER-TELEMETRY"; - - // Endpoints - public const string RISK_API_PATH = "/api/v3/risk"; + public static readonly string EMAIL_ADDRESS_REGEX = @"\A(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\Z"; + // Endpoints + public const string RISK_API_PATH = "/api/v3/risk"; public const string ACTIVITIES_API_PATH = "/api/v1/collector/s2s"; public const string ENFORCER_TELEMETRY_API_PATH = "/api/v2/risk/telemetry"; diff --git a/PerimeterXModule/Internals/PxContext.cs b/PerimeterXModule/Internals/PxContext.cs index c57bac7..7d6b83d 100644 --- a/PerimeterXModule/Internals/PxContext.cs +++ b/PerimeterXModule/Internals/PxContext.cs @@ -51,6 +51,8 @@ public class PxContext public string VidSource { get; set; } public string Pxhd { get; set; } public bool MonitorRequest { get; set; } + public LoginCredentialsFields LoginCredentialsFields { get; set; } + public string RequestId { get; set; } public PxContext(HttpContext context, PxModuleConfigurationSection pxConfiguration) { @@ -61,11 +63,12 @@ public PxContext(HttpContext context, PxModuleConfigurationSection pxConfigurati OriginalTokens = new Dictionary(); S2SCallReason = "none"; IsMobileRequest = false; + RequestId = Guid.NewGuid().ToString(); - // Get Headers + // Get Headers - // if userAgentOverride is present override the default user-agent - CookieNames = extractCookieNames(context.Request.Headers[PxConstants.COOKIE_HEADER]); + // if userAgentOverride is present override the default user-agent + CookieNames = extractCookieNames(context.Request.Headers[PxConstants.COOKIE_HEADER]); string userAgentOverride = pxConfiguration.UserAgentOverride; if (!string.IsNullOrEmpty(userAgentOverride)) { @@ -284,7 +287,19 @@ public Dictionary GetHeadersAsDictionary() return headersDictionary; } - public string MapBlockAction() + public Dictionary GetLowercaseHeadersAsDictionary() + { + Dictionary headersDictionary = new Dictionary(); + + if (Headers != null && Headers.Count() > 0) + { + headersDictionary = Headers.ToDictionary(header => header.Name.ToLower(), header => header.Value); + } + + return headersDictionary; + } + + public string MapBlockAction() { if (string.IsNullOrEmpty(BlockAction)) { @@ -302,5 +317,11 @@ public string MapBlockAction() return "captcha"; } } + + + public bool IsBreachedAccount() + { + return Pxde != null && Pxde.breached_account != null && IsPxdeVerified; + } } } diff --git a/PerimeterXModule/Internals/Validators/PXCookieValidator.cs b/PerimeterXModule/Internals/Validators/PXCookieValidator.cs index b0a095e..3600a6a 100644 --- a/PerimeterXModule/Internals/Validators/PXCookieValidator.cs +++ b/PerimeterXModule/Internals/Validators/PXCookieValidator.cs @@ -103,7 +103,7 @@ public virtual bool Verify(PxContext context, IPxCookie pxCookie) return false; } - if (context.SensitiveRoute) + if (context.SensitiveRoute || context.LoginCredentialsFields != null) { PxLoggingUtils.LogDebug(string.Format("Cookie is valid but is a sensitive route {0}", context.Uri)); context.S2SCallReason = CALL_REASON_SENSITIVE_ROUTE; diff --git a/PerimeterXModule/Internals/Validators/PXS2SValidator.cs b/PerimeterXModule/Internals/Validators/PXS2SValidator.cs index c9357ec..bcd1d4f 100644 --- a/PerimeterXModule/Internals/Validators/PXS2SValidator.cs +++ b/PerimeterXModule/Internals/Validators/PXS2SValidator.cs @@ -110,8 +110,9 @@ public RiskResponse SendRiskResponse(PxContext PxContext) PxCookieHMAC = PxContext.PxCookieHmac, CookieOrigin = PxContext.CookieOrigin, RequestCookieNames = PxContext.CookieNames, - VidSource = PxContext.VidSource - }, + VidSource = PxContext.VidSource, + RequestId = PxContext.RequestId + }, FirstParty = PxConfig.FirstPartyEnabled }; @@ -149,9 +150,28 @@ public RiskResponse SendRiskResponse(PxContext PxContext) riskRequest.Additional.OriginalTokenError = PxContext.OriginalTokenError; } - string requestJson = JSON.SerializeDynamic(riskRequest, PxConstants.JSON_OPTIONS); + SetCredentialsIntelligenceOnRisk(PxContext, riskRequest.Additional); + + string requestJson = JSON.SerializeDynamic(riskRequest, PxConstants.JSON_OPTIONS); var responseJson = httpHandler.Post(requestJson, PxConstants.RISK_API_PATH); return JSON.Deserialize(responseJson, PxConstants.JSON_OPTIONS); } + + public void SetCredentialsIntelligenceOnRisk(PxContext pxContext, Additional riskRequest) + { + LoginCredentialsFields loginCredentialsFields = pxContext.LoginCredentialsFields; + + if (loginCredentialsFields != null) + { + riskRequest.Username = loginCredentialsFields.Username; + riskRequest.CiVersion = loginCredentialsFields.CiVersion; + riskRequest.Password = loginCredentialsFields.Password; + + if (loginCredentialsFields.CiVersion == CIVersion.MULTISTEP_SSO) + { + riskRequest.SsoStep = loginCredentialsFields.SsoStep; + } + } + } } } diff --git a/PerimeterXModule/PerimeterXModule.csproj b/PerimeterXModule/PerimeterXModule.csproj index 7818f2e..1329ad0 100644 --- a/PerimeterXModule/PerimeterXModule.csproj +++ b/PerimeterXModule/PerimeterXModule.csproj @@ -64,6 +64,27 @@ + + + + + + + + + + + + + + + + + + + + + @@ -104,6 +125,7 @@ + diff --git a/PerimeterXModule/Properties/AssemblyInfo.cs b/PerimeterXModule/Properties/AssemblyInfo.cs index 81e00a4..66c8491 100644 --- a/PerimeterXModule/Properties/AssemblyInfo.cs +++ b/PerimeterXModule/Properties/AssemblyInfo.cs @@ -7,7 +7,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("PerimeterX")] [assembly: AssemblyProduct("PxModule")] -[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyCopyright("Copyright © 2023")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] @@ -23,5 +23,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.1.4")] -[assembly: AssemblyFileVersion("3.1.4")] +[assembly: AssemblyVersion("3.2.0")] +[assembly: AssemblyFileVersion("3.2.0")] diff --git a/PerimeterXModule/PxCustomFunctions.cs b/PerimeterXModule/PxCustomFunctions.cs new file mode 100644 index 0000000..f12e721 --- /dev/null +++ b/PerimeterXModule/PxCustomFunctions.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using PerimeterX.CustomBehavior; + +namespace PerimeterX +{ + public static class PxCustomFunctions + { + public static ILoginSuccessfulHandler GetCustomLoginSuccessfulHandler(string customHandlerName) + { + if (string.IsNullOrEmpty(customHandlerName)) + { + return null; + } + + try + { + var customLoginSuccessfulHandler = + getAssembliesTypes().FirstOrDefault(t => t.GetInterface(typeof(ILoginSuccessfulHandler).Name) != null && + t.Name.Equals(customHandlerName) && t.IsClass && !t.IsAbstract); + + if (customLoginSuccessfulHandler != null) + { + var instance = (ILoginSuccessfulHandler)Activator.CreateInstance(customLoginSuccessfulHandler, null); + PxLoggingUtils.LogDebug(string.Format("Successfully loaded ICustomLoginSuccessfulHandler '{0}'.", customHandlerName)); + return instance; + } + else + { + PxLoggingUtils.LogDebug(string.Format( + "Missing implementation of the configured ICustomLoginSuccessfulHandler ('customLoginSuccessfulHandler' attribute): {0}.", + customHandlerName)); + } + } + catch (ReflectionTypeLoadException ex) + { + PxLoggingUtils.LogError(string.Format("Failed to load the ICustomLoginSuccessfulHandler '{0}': {1}.", + customHandlerName, ex.Message)); + } + catch (Exception ex) + { + PxLoggingUtils.LogError(string.Format("Encountered an error while retrieving the ICustomLoginSuccessfulHandler '{0}': {1}.", + customHandlerName, ex.Message)); + } + + return null; + } + + private static IEnumerable getAssembliesTypes() + { + return AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => + { + try + { + return a.GetTypes(); + } + catch + { + return new Type[0]; + } + }); + } + + /// + /// Uses reflection to check whether an IVerificationHandler was implemented by the customer. + /// + /// If found, returns the IVerificationHandler class instance. Otherwise, returns null. + public static IVerificationHandler GetCustomVerificationHandler(string customHandlerName) + { + if (string.IsNullOrEmpty(customHandlerName)) + { + return null; + } + + try + { + var customVerificationHandlerType = getAssembliesTypes() + .FirstOrDefault(t => t.GetInterface(typeof(IVerificationHandler).Name) != null && + t.Name.Equals(customHandlerName) && t.IsClass && !t.IsAbstract); + + if (customVerificationHandlerType != null) + { + var instance = (IVerificationHandler)Activator.CreateInstance(customVerificationHandlerType, null); + PxLoggingUtils.LogDebug(string.Format("Successfully loaded ICustomeVerificationHandler '{0}'.", customHandlerName)); + return instance; + } + else + { + PxLoggingUtils.LogDebug(string.Format( + "Missing implementation of the configured IVerificationHandler ('customVerificationHandler' attribute): {0}.", + customHandlerName)); + } + } + catch (ReflectionTypeLoadException ex) + { + PxLoggingUtils.LogError(string.Format("Failed to load the ICustomeVerificationHandler '{0}': {1}.", + customHandlerName, ex.Message)); + } + catch (Exception ex) + { + PxLoggingUtils.LogError(string.Format("Encountered an error while retrieving the ICustomeVerificationHandler '{0}': {1}.", + customHandlerName, ex.Message)); + } + + return null; + } + + public static ICredentialsExtractionHandler GetCustomLoginCredentialsExtractionHandler(string customHandlerName) + { + if (string.IsNullOrEmpty(customHandlerName)) + { + return null; + } + + try + { + var customCredentialsExtraction = getAssembliesTypes() + .FirstOrDefault(t => t.GetInterface(typeof(ICredentialsExtractionHandler).Name) != null && + t.Name.Equals(customHandlerName) && t.IsClass && !t.IsAbstract); + + if (customCredentialsExtraction != null) + { + var instance = (ICredentialsExtractionHandler)Activator.CreateInstance(customCredentialsExtraction, null); + PxLoggingUtils.LogDebug(string.Format("Successfully loaded ICredentialsExtractionHandler '{0}'.", customHandlerName)); + return instance; + } + else + { + PxLoggingUtils.LogDebug(string.Format( + "Missing implementation of the configured IVerificationHandler ('ICredentialsExtractionHandler' attribute): {0}.", + customHandlerName)); + } + } + catch (ReflectionTypeLoadException ex) + { + PxLoggingUtils.LogError(string.Format("Failed to load the ICredentialsExtractionHandler '{0}': {1}.", + customHandlerName, ex.Message)); + } + catch (Exception ex) + { + PxLoggingUtils.LogError(string.Format("Encountered an error while retrieving the ICredentialsExtractionHandler '{0}': {1}.", + customHandlerName, ex.Message)); + } + + return null; + } + } +} \ No newline at end of file diff --git a/PerimeterXModule/PxModule.cs b/PerimeterXModule/PxModule.cs index 50a2440..9dab783 100644 --- a/PerimeterXModule/PxModule.cs +++ b/PerimeterXModule/PxModule.cs @@ -1,629 +1,697 @@ -// Copyright � 2016 PerimeterX, Inc. -// -// Permission is hereby granted, free of charge, to any -// person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the -// Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the -// Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice -// shall be included in all copies or substantial portions of -// the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -// KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -// PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE AUTHORS -// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Web; -using System.Security.Cryptography; -using System.Text; -using System.IO; -using System.Configuration; -using System.Diagnostics; -using System.Collections.Specialized; -using System.Net; -using System.Linq; -using System.Reflection; -using System.Collections; -using Jil; -using System.Collections.Generic; -using PerimeterX.Internals; -using System.Web.Script.Serialization; - -namespace PerimeterX -{ - public class PxModule : IHttpModule - { - private HttpHandler httpHandler; - private PxContext pxContext; - private static IActivityReporter reporter; - private readonly string validationMarker; - private readonly ICookieDecoder cookieDecoder; - private readonly IPXCookieValidator PxCookieValidator; - private readonly IPXS2SValidator PxS2SValidator; - private readonly IReverseProxy ReverseProxy; - private readonly PxBlock pxBlock; - - private readonly bool enabled; - private readonly bool sendPageActivites; - private readonly bool sendBlockActivities; - private readonly int blockingScore; - private readonly string appId; - private readonly IVerificationHandler customVerificationHandlerInstance; - private readonly bool suppressContentBlock; - private readonly bool challengeEnabled; - private readonly string[] sensetiveHeaders; - private readonly StringCollection fileExtWhitelist; - private readonly StringCollection routesWhitelist; - private readonly StringCollection useragentsWhitelist; - private readonly StringCollection enforceSpecificRoutes; - private readonly string cookieKey; - private readonly string customBlockUrl; - private readonly byte[] cookieKeyBytes; - private readonly string osVersion; - private string nodeName; - - static PxModule() - { - try - { - var config = (PxModuleConfigurationSection)ConfigurationManager.GetSection(PxConstants.CONFIG_SECTION); - PxLoggingUtils.init(config.AppId); - // allocate reporter if needed - if (config != null && (config.SendBlockActivites || config.SendPageActivites)) - { - reporter = new ActivityReporter(PxConstants.FormatBaseUri(config), config.ActivitiesCapacity, config.ActivitiesBulkSize, config.ReporterApiTimeout); - } - else - { - reporter = new NullActivityMonitor(); - } - } - catch (Exception ex) - { - PxLoggingUtils.LogDebug("Failed to extract assembly version " + ex.Message); - } - } - - public PxModule() - { - var config = (PxModuleConfigurationSection)ConfigurationManager.GetSection(PxConstants.CONFIG_SECTION); - if (config == null) - { - throw new ConfigurationErrorsException("Missing PerimeterX module configuration section " + PxConstants.CONFIG_SECTION); - } - - // load configuration - enabled = config.Enabled; - sendPageActivites = config.SendPageActivites; - sendBlockActivities = config.SendBlockActivites; - cookieKey = config.CookieKey; - cookieKeyBytes = Encoding.UTF8.GetBytes(cookieKey); - blockingScore = config.BlockingScore; - appId = config.AppId; - customVerificationHandlerInstance = GetCustomVerificationHandler(config.CustomVerificationHandler); - suppressContentBlock = config.SuppressContentBlock; - challengeEnabled = config.ChallengeEnabled; - sensetiveHeaders = config.SensitiveHeaders.Cast().ToArray(); - fileExtWhitelist = config.FileExtWhitelist; - routesWhitelist = config.RoutesWhitelist; - useragentsWhitelist = config.UseragentsWhitelist; - enforceSpecificRoutes = config.EnforceSpecificRoutes; - - // Set Decoder - if (config.EncryptionEnabled) - { - cookieDecoder = new EncryptedCookieDecoder(cookieKeyBytes); - } - else - { - cookieDecoder = new CookieDecoder(); - } - - using (var hasher = new SHA256Managed()) - { - validationMarker = ByteArrayToHexString(hasher.ComputeHash(cookieKeyBytes)); - } - - this.httpHandler = new HttpHandler(config, PxConstants.FormatBaseUri(config), config.ApiTimeout); - - // Set Validators - PxS2SValidator = new PXS2SValidator(config, httpHandler); - PxCookieValidator = new PXCookieValidator(config) - { - PXOriginalTokenValidator = new PXOriginalTokenValidator(config) - }; - - // Get OS type - osVersion = Environment.OSVersion.VersionString; - - // Build reverse proxy - ReverseProxy = new ReverseProxy(config); - - pxBlock = new PxBlock(config); - - PxLoggingUtils.LogDebug(ModuleName + " initialized"); - } - - public string ModuleName - { - get { return "PxModule"; } - } - - public void Init(HttpApplication application) - { - application.BeginRequest += this.Application_BeginRequest; - nodeName = application.Context.Server.MachineName; - } - - private void Application_BeginRequest(object source, EventArgs e) - { - try - { - var application = (HttpApplication)source; - - if (application == null) - { - return; - } - - var applicationContext = application.Context; - - if (applicationContext == null || IsFirstPartyProxyRequest(applicationContext)) - { - return; - } - - if (IsFilteredRequest(applicationContext)) - { - return; - } - try - { - //check if this is a telemetry command - if (IsTelemetryCommand(applicationContext)) - { - //command is valid. send telemetry - PostEnforcerTelemetryActivity(); - } - } - catch (Exception ex) - { - PxLoggingUtils.LogDebug("Failed to validate Telemetry command request: " + ex.Message); - } - - if (validationMarker == applicationContext.Request.Headers[PxConstants.PX_VALIDATED_HEADER]) - { - return; - } - - // Setting custom header for classic mode - if (HttpRuntime.UsingIntegratedPipeline) - { - applicationContext.Request.Headers.Add(PxConstants.PX_VALIDATED_HEADER, validationMarker); - } - else - { - var headers = applicationContext.Request.Headers; - Type hdr = headers.GetType(); - PropertyInfo ro = hdr.GetProperty("IsReadOnly", - BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.IgnoreCase | BindingFlags.FlattenHierarchy); - // Remove the ReadOnly property - ro.SetValue(headers, false, null); - // Invoke the protected InvalidateCachedArrays method - hdr.InvokeMember("InvalidateCachedArrays", - BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, - null, headers, null); - // Now invoke the protected "BaseAdd" method of the base class to add the - // headers you need. The header content needs to be an ArrayList or the - // the web application will choke on it. - hdr.InvokeMember("BaseAdd", - BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, - null, headers, - new object[] { PxConstants.PX_VALIDATED_HEADER, new ArrayList { validationMarker } }); - // repeat BaseAdd invocation for any other headers to be added - // Then set the collection back to ReadOnly - ro.SetValue(headers, true, null); - } - - VerifyRequest(application); - } - catch (Exception ex) - { - PxLoggingUtils.LogDebug("Failed to validate request: " + ex.Message); - } - } - - private bool IsTelemetryCommand(HttpContext applicationContext) - { - //extract header value and decode it from base64 string - string headerValue = applicationContext.Request.Headers[PxConstants.ENFORCER_TELEMETRY_HEADER]; - if(headerValue == null) - { - return false; - } - - //we got Telemetry command request - PxLoggingUtils.LogDebug("Received command to send enforcer telemetry"); - - //base 64 decode - string decodedString = Encoding.UTF8.GetString(Convert.FromBase64String(headerValue)); - - //value is in the form of timestamp:hmac_val - string[] splittedValue = decodedString.Split(new Char[] { ':' }); - if (splittedValue.Length != 2) - { - PxLoggingUtils.LogDebug("Malformed header value - " + PxConstants.ENFORCER_TELEMETRY_HEADER + " = " + headerValue); - return false; - } - - //timestamp - DateTime expirationTime = (new DateTime(1970, 1, 1)).AddMilliseconds(double.Parse(splittedValue[0])); - if ((expirationTime - DateTime.UtcNow).Ticks < 0) - { - //commmand is expired - PxLoggingUtils.LogDebug("Telemetry command is expired"); - return false; - } - - //check hmac integrity - string generatedHmac = BitConverter.ToString(new HMACSHA256(cookieKeyBytes).ComputeHash(Encoding.UTF8.GetBytes(splittedValue[0]))).Replace("-", ""); - if (generatedHmac != splittedValue[1].ToUpper()) - { - PxLoggingUtils.LogDebug("hmac validation failed. original = " + splittedValue[1] + ", generated = " + generatedHmac); - return false; - } - - return true; - } - - private void PostPageRequestedActivity(PxContext pxContext) - { - if (sendPageActivites) - { - PostActivity(pxContext, "page_requested", new ActivityDetails - { - ModuleVersion = PxConstants.MODULE_VERSION, - PassReason = pxContext.PassReason, - RiskRoundtripTime = pxContext.RiskRoundtripTime, +// Copyright � 2016 PerimeterX, Inc. +// +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the +// Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions of +// the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +// KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE AUTHORS +// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Web; +using System.Security.Cryptography; +using System.Text; +using System.IO; +using System.Configuration; +using System.Collections.Specialized; +using System.Net; +using System.Linq; +using System.Reflection; +using System.Collections; +using Jil; +using System.Collections.Generic; +using PerimeterX.Internals; +using System.Web.Script.Serialization; +using PerimeterX.CustomBehavior; +using static System.Net.Mime.MediaTypeNames; + +namespace PerimeterX +{ + public class PxModule : IHttpModule + { + private HttpHandler httpHandler; + private PxContext pxContext; + private static IActivityReporter reporter; + private readonly string validationMarker; + private readonly ICookieDecoder cookieDecoder; + private readonly IPXCookieValidator PxCookieValidator; + private readonly IPXS2SValidator PxS2SValidator; + private readonly IReverseProxy ReverseProxy; + private readonly PxBlock pxBlock; + + private readonly bool enabled; + private readonly bool sendPageActivites; + private readonly bool sendBlockActivities; + private readonly int blockingScore; + private readonly string appId; + private readonly bool suppressContentBlock; + private readonly bool challengeEnabled; + private readonly string[] sensetiveHeaders; + private readonly StringCollection fileExtWhitelist; + private readonly StringCollection routesWhitelist; + private readonly StringCollection useragentsWhitelist; + private readonly StringCollection enforceSpecificRoutes; + private readonly string cookieKey; + private readonly string customBlockUrl; + private readonly byte[] cookieKeyBytes; + private readonly string osVersion; + private string nodeName; + private bool loginCredentialsExtractionEnabled; + private CredentialIntelligenceManager loginData; + private IVerificationHandler customVerificationHandlerInstance; + private ICredentialsExtractionHandler customCredentialsExtraction; + + static PxModule() + { + try + { + var config = (PxModuleConfigurationSection)ConfigurationManager.GetSection(PxConstants.CONFIG_SECTION); + PxLoggingUtils.init(config.AppId); + // allocate reporter if needed + if (config != null && (config.SendBlockActivites || config.SendPageActivites)) + { + reporter = new ActivityReporter(PxConstants.FormatBaseUri(config), config.ActivitiesCapacity, config.ActivitiesBulkSize, config.ReporterApiTimeout); + } + else + { + reporter = new NullActivityMonitor(); + } + } + catch (Exception ex) + { + PxLoggingUtils.LogDebug("Failed to extract assembly version " + ex.Message); + } + } + + public PxModule() + { + var config = (PxModuleConfigurationSection)ConfigurationManager.GetSection(PxConstants.CONFIG_SECTION); + if (config == null) + { + throw new ConfigurationErrorsException("Missing PerimeterX module configuration section " + PxConstants.CONFIG_SECTION); + } + + // load configuration + enabled = config.Enabled; + sendPageActivites = config.SendPageActivites; + sendBlockActivities = config.SendBlockActivites; + cookieKey = config.CookieKey; + cookieKeyBytes = Encoding.UTF8.GetBytes(cookieKey); + blockingScore = config.BlockingScore; + appId = config.AppId; + customVerificationHandlerInstance = PxCustomFunctions.GetCustomVerificationHandler(config.CustomVerificationHandler); + suppressContentBlock = config.SuppressContentBlock; + challengeEnabled = config.ChallengeEnabled; + sensetiveHeaders = config.SensitiveHeaders.Cast().ToArray(); + fileExtWhitelist = config.FileExtWhitelist; + routesWhitelist = config.RoutesWhitelist; + useragentsWhitelist = config.UseragentsWhitelist; + enforceSpecificRoutes = config.EnforceSpecificRoutes; + + InitCredentialsIntelligence(config); + + // Set Decoder + if (config.EncryptionEnabled) + { + cookieDecoder = new EncryptedCookieDecoder(cookieKeyBytes); + } + else + { + cookieDecoder = new CookieDecoder(); + } + + using (var hasher = new SHA256Managed()) + { + validationMarker = ByteArrayToHexString(hasher.ComputeHash(cookieKeyBytes)); + } + + this.httpHandler = new HttpHandler(config, PxConstants.FormatBaseUri(config), config.ApiTimeout); + + // Set Validators + PxS2SValidator = new PXS2SValidator(config, httpHandler); + PxCookieValidator = new PXCookieValidator(config) + { + PXOriginalTokenValidator = new PXOriginalTokenValidator(config) + }; + + // Get OS type + osVersion = Environment.OSVersion.VersionString; + + // Build reverse proxy + ReverseProxy = new ReverseProxy(config); + + pxBlock = new PxBlock(config); + + PxLoggingUtils.LogDebug(ModuleName + " initialized"); + } + + public string ModuleName + { + get { return "PxModule"; } + } + + private void InitCredentialsIntelligence(PxModuleConfigurationSection config) + { + try + { + List loginCredentialsExtraction; + loginCredentialsExtractionEnabled = config.LoginCredentialsExtractionEnabled; + + if (loginCredentialsExtractionEnabled && config.LoginCredentialsExtraction != "") + { + loginCredentialsExtraction = JSON.Deserialize>(config.LoginCredentialsExtraction, PxConstants.JSON_OPTIONS); + loginData = new CredentialIntelligenceManager(config.CiVersion, loginCredentialsExtraction); + customCredentialsExtraction = PxCustomFunctions.GetCustomLoginCredentialsExtractionHandler(config.CustomCredentialsExtractionHandler); + } + } catch(Exception ex) + { + PxLoggingUtils.LogDebug("An error occurred while initiating the CI configuration " + ex.Message); + } + } + + public void Init(HttpApplication application) + { + application.BeginRequest += this.Application_BeginRequest; + nodeName = application.Context.Server.MachineName; + application.EndRequest += this.Application_EndRequest; + } + + private void Application_BeginRequest(object source, EventArgs e) + { + try + { + HttpResponse response = HttpContext.Current.Response; + response.Filter = new OutputFilterStream(response.Filter); + + var application = (HttpApplication)source; + + if (application == null) + { + return; + } + + var applicationContext = application.Context; + + if (applicationContext == null || IsFirstPartyProxyRequest(applicationContext)) + { + return; + } + + if (IsFilteredRequest(applicationContext)) + { + return; + } + try + { + //check if this is a telemetry command + if (IsTelemetryCommand(applicationContext)) + { + //command is valid. send telemetry + PostEnforcerTelemetryActivity(); + } + } + catch (Exception ex) + { + PxLoggingUtils.LogDebug("Failed to validate Telemetry command request: " + ex.Message); + } + + if (validationMarker == applicationContext.Request.Headers[PxConstants.PX_VALIDATED_HEADER]) + { + return; + } + + // Setting custom header for classic mode + if (HttpRuntime.UsingIntegratedPipeline) + { + applicationContext.Request.Headers.Add(PxConstants.PX_VALIDATED_HEADER, validationMarker); + } + else + { + var headers = applicationContext.Request.Headers; + Type hdr = headers.GetType(); + PropertyInfo ro = hdr.GetProperty("IsReadOnly", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.IgnoreCase | BindingFlags.FlattenHierarchy); + // Remove the ReadOnly property + ro.SetValue(headers, false, null); + // Invoke the protected InvalidateCachedArrays method + hdr.InvokeMember("InvalidateCachedArrays", + BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, + null, headers, null); + // Now invoke the protected "BaseAdd" method of the base class to add the + // headers you need. The header content needs to be an ArrayList or the + // the web application will choke on it. + hdr.InvokeMember("BaseAdd", + BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, + null, headers, + new object[] { PxConstants.PX_VALIDATED_HEADER, new ArrayList { validationMarker } }); + // repeat BaseAdd invocation for any other headers to be added + // Then set the collection back to ReadOnly + ro.SetValue(headers, true, null); + } + + VerifyRequest(application); + } + catch (Exception ex) + { + PxLoggingUtils.LogDebug("Failed to validate request: " + ex.Message); + } + } + + private void Application_EndRequest(object source, EventArgs e) + { + var application = (HttpApplication)source; + + if (application == null) + { + return; + } + + try + { + var applicationContext = application.Context; + var config = (PxModuleConfigurationSection)ConfigurationManager.GetSection(PxConstants.CONFIG_SECTION); + + if (!config.AdditionalS2SActivityHeaderEnabled && pxContext != null && pxContext.LoginCredentialsFields != null) + { + bool? isLoginSuccessful = HandleLoginSuccessful(applicationContext.Response, config, application); + HandleAutomaticAdditionalS2SActivity(applicationContext.Response, config, isLoginSuccessful); + } + } + catch (Exception ex) + { + PxLoggingUtils.LogDebug("Failed to handle end reqeust event: " + ex.Message); + } + } + + public bool? HandleLoginSuccessful(HttpResponse httpResponse, PxModuleConfigurationSection config, HttpApplication application) + { + try + { + ILoginSuccessfulParser loginSuccessfulParser = LoginSuccessfulParserFactory.Create(config); + bool? isLoginSuccessful = loginSuccessfulParser.IsLoginSuccessful(httpResponse); + + if (loginSuccessfulParser != null && isLoginSuccessful.HasValue) + { + return isLoginSuccessful; + } + } + catch (Exception ex) + { + PxLoggingUtils.LogDebug("Error determining login status: " + ex.Message); + } + + return null; + } + + private void HandleAutomaticAdditionalS2SActivity(HttpResponse httpResponse, PxModuleConfigurationSection config, bool? isLoginSuccessful) + { + reporter.Post(AdditionalS2SUtils.CreateAdditionalS2SActivity(config, httpResponse.StatusCode, isLoginSuccessful, pxContext)); + } + + private bool IsTelemetryCommand(HttpContext applicationContext) + { + //extract header value and decode it from base64 string + string headerValue = applicationContext.Request.Headers[PxConstants.ENFORCER_TELEMETRY_HEADER]; + if(headerValue == null) + { + return false; + } + + //we got Telemetry command request + PxLoggingUtils.LogDebug("Received command to send enforcer telemetry"); + + //base 64 decode + string decodedString = Encoding.UTF8.GetString(Convert.FromBase64String(headerValue)); + + //value is in the form of timestamp:hmac_val + string[] splittedValue = decodedString.Split(new Char[] { ':' }); + if (splittedValue.Length != 2) + { + PxLoggingUtils.LogDebug("Malformed header value - " + PxConstants.ENFORCER_TELEMETRY_HEADER + " = " + headerValue); + return false; + } + + //timestamp + DateTime expirationTime = (new DateTime(1970, 1, 1)).AddMilliseconds(double.Parse(splittedValue[0])); + if ((expirationTime - DateTime.UtcNow).Ticks < 0) + { + //commmand is expired + PxLoggingUtils.LogDebug("Telemetry command is expired"); + return false; + } + + //check hmac integrity + string generatedHmac = BitConverter.ToString(new HMACSHA256(cookieKeyBytes).ComputeHash(Encoding.UTF8.GetBytes(splittedValue[0]))).Replace("-", ""); + if (generatedHmac != splittedValue[1].ToUpper()) + { + PxLoggingUtils.LogDebug("hmac validation failed. original = " + splittedValue[1] + ", generated = " + generatedHmac); + return false; + } + + return true; + } + + private void PostPageRequestedActivity(PxContext pxContext) + { + if (sendPageActivites) + { + PostActivity(pxContext, "page_requested", new ActivityDetails + { + ModuleVersion = PxConstants.MODULE_VERSION, + PassReason = pxContext.PassReason, + RiskRoundtripTime = pxContext.RiskRoundtripTime, ClientUuid = pxContext.UUID, - httpMethod = pxContext.HttpMethod - }); - } - } - - private void PostBlockActivity(PxContext pxContext, PxModuleConfigurationSection config) - { - if (sendBlockActivities) - { - PostActivity(pxContext, "block", new ActivityDetails - { - BlockAction = pxContext.BlockAction, - BlockReason = pxContext.BlockReason, - BlockUuid = pxContext.UUID, - ModuleVersion = PxConstants.MODULE_VERSION, - RiskScore = pxContext.Score, + httpMethod = pxContext.HttpMethod + }); + } + } + + private void PostBlockActivity(PxContext pxContext, PxModuleConfigurationSection config) + { + if (sendBlockActivities) + { + PostActivity(pxContext, "block", new ActivityDetails + { + BlockAction = pxContext.BlockAction, + BlockReason = pxContext.BlockReason, + BlockUuid = pxContext.UUID, + ModuleVersion = PxConstants.MODULE_VERSION, + RiskScore = pxContext.Score, RiskRoundtripTime = pxContext.RiskRoundtripTime, httpMethod = pxContext.HttpMethod, SimulatedBlock = config.MonitorMode == true - }); - } - } - - private void PostEnforcerTelemetryActivity() - { - var config = (PxModuleConfigurationSection)ConfigurationManager.GetSection(PxConstants.CONFIG_SECTION); - - string serializedConfig; - using (var json = new StringWriter()) - { - JSON.Serialize(config, json); - serializedConfig = json.ToString(); - } - - //remove cookieKey and ApiToken from telemetry - var jsSerializer = new JavaScriptSerializer(); - Dictionary dict = (Dictionary)jsSerializer.DeserializeObject(serializedConfig); - dict.Remove("CookieKey"); - dict.Remove("ApiToken"); - serializedConfig = new JavaScriptSerializer().Serialize(dict); - - var activity = new Activity - { - Type = "enforcer_telemetry", - Timestamp = PxConstants.GetTimestamp(), - AppId = appId, - Details = new EnforcerTelemetryActivityDetails - { - ModuleVersion = PxConstants.MODULE_VERSION, - UpdateReason = EnforcerTelemetryUpdateReasonEnum.COMMAND, - OsName = osVersion, - NodeName = nodeName, - EnforcerConfigs = serializedConfig - } - }; - - try - { - var stringBuilder = new StringBuilder(); - using (var stringOutput = new StringWriter(stringBuilder)) - { - JSON.SerializeDynamic(activity, stringOutput, Options.ExcludeNullsIncludeInherited); - } - - httpHandler.Post(stringBuilder.ToString(), PxConstants.ENFORCER_TELEMETRY_API_PATH); - } - catch (Exception ex) - { - PxLoggingUtils.LogDebug(string.Format("Encountered an error sending enforcer telemetry activity: {0}.", ex.Message)); - } - } - - private void PostActivity(PxContext pxContext, string eventType, ActivityDetails details = null) - { - var activity = new Activity - { - Type = eventType, - Timestamp = PxConstants.GetTimestamp(), - AppId = appId, - SocketIP = pxContext.Ip, - Url = pxContext.FullUrl, - Details = details, - Headers = pxContext.GetHeadersAsDictionary(), - }; - - - if (!string.IsNullOrEmpty(pxContext.Vid)) - { - activity.Vid = pxContext.Vid; - } - - if (!string.IsNullOrEmpty(pxContext.Pxhd) && (eventType == "page_requested" || eventType == "block")) - { - activity.Pxhd = pxContext.Pxhd; - } - - reporter.Post(activity); - } - - public void BlockRequest(PxContext pxContext, PxModuleConfigurationSection config) - { - pxContext.ApplicationContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - pxContext.ApplicationContext.Response.TrySkipIisCustomErrors = true; - if (config.SuppressContentBlock) - { - pxContext.ApplicationContext.Response.SuppressContent = true; - } - else - { - pxBlock.ResponseBlockPage(pxContext, config); - } - } - - public void Dispose() - { - if (httpHandler != null) - { - httpHandler.Dispose(); - } - } - - - /// - /// Checks the url if it should be a proxy request for the client/xhrs - /// - /// HTTP context for client - private bool IsFirstPartyProxyRequest(HttpContext context) - { - return ReverseProxy.ShouldReverseClient(context) || ReverseProxy.ShouldReverseCaptcha(context) || ReverseProxy.ShouldReverseXhr(context); - } - - private bool IsFilteredRequest(HttpContext context) - { - if (!enabled) - { - return true; - } - - - // whitelist file extension - var ext = Path.GetExtension(context.Request.Url.AbsolutePath).ToLowerInvariant(); - if (fileExtWhitelist != null && fileExtWhitelist.Contains(ext)) - { - return true; - } - - var url = context.Request.Url.AbsolutePath; - - // custom block url check - if (customBlockUrl != null && url == customBlockUrl) { - return true; - } - - // whitelist routes prefix - if (routesWhitelist != null) - { - foreach (var prefix in routesWhitelist) - { - if (url.StartsWith(prefix)) - { - return true; - } - } - } - - // whitelist user-agent - if (useragentsWhitelist != null && useragentsWhitelist.Contains(context.Request.UserAgent)) - { - return true; - } - - // enforce specific routes prefix - if (enforceSpecificRoutes != null) - { - // case list is not empty, module will skip the route if - // the routes prefix is not present in the list - foreach (var prefix in enforceSpecificRoutes) - { - if (url.StartsWith(prefix)) - { - return false; - } - } - // we go over all the list and prefix wasn't found - // meaning this route is not a specifc route - return true; - } - - return false; - } - - private void VerifyRequest(HttpApplication application) - { - try - { - var config = (PxModuleConfigurationSection)ConfigurationManager.GetSection(PxConstants.CONFIG_SECTION); - pxContext = new PxContext(application.Context, config); - - // validate using risk cookie - IPxCookie pxCookie = PxCookieUtils.BuildCookie(config, pxContext.PxCookies, cookieDecoder); - pxContext.OriginalToken = PxCookieUtils.BuildCookie(config, pxContext.OriginalTokens, cookieDecoder); - if (!PxCookieValidator.Verify(pxContext, pxCookie)) - { - // validate using server risk api - PxS2SValidator.VerifyS2S(pxContext); - } - - HandleVerification(application); - } - catch (Exception ex) // Fail-open approach - { - PxLoggingUtils.LogError(string.Format("Module failed to process request in fault: {0}, passing request", ex.Message)); - pxContext.PassReason = PassReasonEnum.ERROR; - PostPageRequestedActivity(pxContext); - } - } - - private static string ByteArrayToHexString(byte[] input) - { - StringBuilder sb = new StringBuilder(input.Length * 2); - foreach (byte b in input) - { - sb.Append(PxConstants.HEX_ALPHABET[b >> 4]); - sb.Append(PxConstants.HEX_ALPHABET[b & 0xF]); - } - return sb.ToString(); - } - - private void HandleVerification(HttpApplication application) - { - PxModuleConfigurationSection config = (PxModuleConfigurationSection)ConfigurationManager.GetSection(PxConstants.CONFIG_SECTION); - bool verified = blockingScore > pxContext.Score; - - PxLoggingUtils.LogDebug(string.Format("Request score: {0}, blocking score: {1}, monitor mode status: {2}.", pxContext.Score, blockingScore, config.MonitorMode == true ? "On" : "Off")); - - if (verified) - { - if (config.MonitorMode) - { - PxLoggingUtils.LogDebug("Monitor Mode is activated. passing request"); - } - PxLoggingUtils.LogDebug(string.Format("Valid request to {0}", application.Context.Request.RawUrl)); - PostPageRequestedActivity(pxContext); - } - else - { - PxLoggingUtils.LogDebug(string.Format("Invalid request to {0}", application.Context.Request.RawUrl)); - PostBlockActivity(pxContext, config); - } - - SetPxhdAndVid(pxContext); - // If implemented, run the customVerificationHandler. - if (customVerificationHandlerInstance != null) - { - customVerificationHandlerInstance.Handle(application, pxContext, config); - } - // No custom verification handler -> continue regular flow - else if (!verified && !pxContext.MonitorRequest) - { - BlockRequest(pxContext, config); - application.CompleteRequest(); - } - } - - private static void SetPxhdAndVid(PxContext pxContext) - { - - if (!string.IsNullOrEmpty(pxContext.Pxhd)) - { - string pxhd = PxConstants.COOKIE_PXHD_PREFIX + "=" + pxContext.Pxhd + "; path=/"; - pxContext.ApplicationContext.Response.AddHeader("Set-Cookie", pxhd); - } - } - - /// - /// Uses reflection to check whether an IVerificationHandler was implemented by the customer. - /// - /// If found, returns the IVerificationHandler class instance. Otherwise, returns null. - private static IVerificationHandler GetCustomVerificationHandler(string customHandlerName) - { - if (string.IsNullOrEmpty(customHandlerName)) - { - return null; - } - - try - { - var customVerificationHandlerType = - AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(a => { - try - { - return a.GetTypes(); - } - catch - { - return new Type[0]; - } - }) - .FirstOrDefault(t => t.GetInterface(typeof(IVerificationHandler).Name) != null && - t.Name.Equals(customHandlerName) && t.IsClass && !t.IsAbstract); - - if (customVerificationHandlerType != null) - { - var instance = (IVerificationHandler)Activator.CreateInstance(customVerificationHandlerType, null); - PxLoggingUtils.LogDebug(string.Format("Successfully loaded ICustomeVerificationHandler '{0}'.", customHandlerName)); - return instance; - } - else - { - PxLoggingUtils.LogDebug(string.Format( - "Missing implementation of the configured IVerificationHandler ('customVerificationHandler' attribute): {0}.", - customHandlerName)); - } - } - catch (ReflectionTypeLoadException ex) - { - PxLoggingUtils.LogError(string.Format("Failed to load the ICustomeVerificationHandler '{0}': {1}.", - customHandlerName, ex.Message)); - } - catch (Exception ex) - { - PxLoggingUtils.LogError(string.Format("Encountered an error while retrieving the ICustomeVerificationHandler '{0}': {1}.", - customHandlerName, ex.Message)); - } - - return null; - } - } -} + }); + } + } + + private void PostEnforcerTelemetryActivity() + { + var config = (PxModuleConfigurationSection)ConfigurationManager.GetSection(PxConstants.CONFIG_SECTION); + + string serializedConfig; + using (var json = new StringWriter()) + { + JSON.Serialize(config, json); + serializedConfig = json.ToString(); + } + + //remove cookieKey and ApiToken from telemetry + var jsSerializer = new JavaScriptSerializer(); + Dictionary dict = (Dictionary)jsSerializer.DeserializeObject(serializedConfig); + dict.Remove("CookieKey"); + dict.Remove("ApiToken"); + serializedConfig = new JavaScriptSerializer().Serialize(dict); + + var activity = new Activity + { + Type = "enforcer_telemetry", + Timestamp = PxConstants.GetTimestamp(), + AppId = appId, + Details = new EnforcerTelemetryActivityDetails + { + ModuleVersion = PxConstants.MODULE_VERSION, + UpdateReason = EnforcerTelemetryUpdateReasonEnum.COMMAND, + OsName = osVersion, + NodeName = nodeName, + EnforcerConfigs = serializedConfig + } + }; + + try + { + var stringBuilder = new StringBuilder(); + using (var stringOutput = new StringWriter(stringBuilder)) + { + JSON.SerializeDynamic(activity, stringOutput, Options.ExcludeNullsIncludeInherited); + } + + httpHandler.Post(stringBuilder.ToString(), PxConstants.ENFORCER_TELEMETRY_API_PATH); + } + catch (Exception ex) + { + PxLoggingUtils.LogDebug(string.Format("Encountered an error sending enforcer telemetry activity: {0}.", ex.Message)); + } + } + + private void PostActivity(PxContext pxContext, string eventType, ActivityDetails details = null) + { + if (pxContext.LoginCredentialsFields != null) + { + LoginCredentialsFields loginCredentialsFields = pxContext.LoginCredentialsFields; + details.CiVersion = loginCredentialsFields.CiVersion; + details.CredentialsCompromised = pxContext.IsBreachedAccount(); + + if (loginCredentialsFields.CiVersion == CIVersion.MULTISTEP_SSO) + { + details.SsoStep = loginCredentialsFields.SsoStep; + } + } + + details.RequestId = pxContext.RequestId; + + var activity = new Activity + { + Type = eventType, + Timestamp = PxConstants.GetTimestamp(), + AppId = appId, + SocketIP = pxContext.Ip, + Url = pxContext.FullUrl, + Details = details, + Headers = pxContext.GetHeadersAsDictionary(), + }; + + if (!string.IsNullOrEmpty(pxContext.Vid)) + { + activity.Vid = pxContext.Vid; + } + + if (!string.IsNullOrEmpty(pxContext.Pxhd) && (eventType == "page_requested" || eventType == "block")) + { + activity.Pxhd = pxContext.Pxhd; + } + + reporter.Post(activity); + } + + public void BlockRequest(PxContext pxContext, PxModuleConfigurationSection config) + { + pxContext.ApplicationContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + pxContext.ApplicationContext.Response.TrySkipIisCustomErrors = true; + if (config.SuppressContentBlock) + { + pxContext.ApplicationContext.Response.SuppressContent = true; + } + else + { + pxBlock.ResponseBlockPage(pxContext, config); + } + } + + public void Dispose() + { + if (httpHandler != null) + { + httpHandler.Dispose(); + } + } + + + /// + /// Checks the url if it should be a proxy request for the client/xhrs + /// + /// HTTP context for client + private bool IsFirstPartyProxyRequest(HttpContext context) + { + return ReverseProxy.ShouldReverseClient(context) || ReverseProxy.ShouldReverseCaptcha(context) || ReverseProxy.ShouldReverseXhr(context); + } + + private bool IsFilteredRequest(HttpContext context) + { + if (!enabled) + { + return true; + } + + + // whitelist file extension + var ext = Path.GetExtension(context.Request.Url.AbsolutePath).ToLowerInvariant(); + if (fileExtWhitelist != null && fileExtWhitelist.Contains(ext)) + { + return true; + } + + var url = context.Request.Url.AbsolutePath; + + // custom block url check + if (customBlockUrl != null && url == customBlockUrl) { + return true; + } + + // whitelist routes prefix + if (routesWhitelist != null) + { + foreach (var prefix in routesWhitelist) + { + if (url.StartsWith(prefix)) + { + return true; + } + } + } + + // whitelist user-agent + if (useragentsWhitelist != null && useragentsWhitelist.Contains(context.Request.UserAgent)) + { + return true; + } + + // enforce specific routes prefix + if (enforceSpecificRoutes != null) + { + // case list is not empty, module will skip the route if + // the routes prefix is not present in the list + foreach (var prefix in enforceSpecificRoutes) + { + if (url.StartsWith(prefix)) + { + return false; + } + } + // we go over all the list and prefix wasn't found + // meaning this route is not a specifc route + return true; + } + + return false; + } + + private void VerifyRequest(HttpApplication application) + { + try + { + var config = (PxModuleConfigurationSection)ConfigurationManager.GetSection(PxConstants.CONFIG_SECTION); + pxContext = new PxContext(application.Context, config); + + if (loginData != null) + { + LoginCredentialsFields loginCredentialsFields = loginData.ExtractCredentialsFromRequest(pxContext, application.Context.Request, customCredentialsExtraction); + if (loginCredentialsFields != null) + { + pxContext.LoginCredentialsFields = loginCredentialsFields; + } + } + + // validate using risk cookie + IPxCookie pxCookie = PxCookieUtils.BuildCookie(config, pxContext.PxCookies, cookieDecoder); + pxContext.OriginalToken = PxCookieUtils.BuildCookie(config, pxContext.OriginalTokens, cookieDecoder); + if (!PxCookieValidator.Verify(pxContext, pxCookie)) + { + // validate using server risk api + PxS2SValidator.VerifyS2S(pxContext); + } + + HandleVerification(application); + } + catch (Exception ex) // Fail-open approach + { + PxLoggingUtils.LogError(string.Format("Module failed to process request in fault: {0}, passing request", ex.Message)); + pxContext.PassReason = PassReasonEnum.ERROR; + PostPageRequestedActivity(pxContext); + } + } + + private static string ByteArrayToHexString(byte[] input) + { + StringBuilder sb = new StringBuilder(input.Length * 2); + foreach (byte b in input) + { + sb.Append(PxConstants.HEX_ALPHABET[b >> 4]); + sb.Append(PxConstants.HEX_ALPHABET[b & 0xF]); + } + return sb.ToString(); + } + + private void HandleVerification(HttpApplication application) + { + PxModuleConfigurationSection config = (PxModuleConfigurationSection)ConfigurationManager.GetSection(PxConstants.CONFIG_SECTION); + bool verified = blockingScore > pxContext.Score; + + PxLoggingUtils.LogDebug(string.Format("Request score: {0}, blocking score: {1}, monitor mode status: {2}.", pxContext.Score, blockingScore, config.MonitorMode == true ? "On" : "Off")); + + if (verified) + { + if (config.MonitorMode) + { + PxLoggingUtils.LogDebug("Monitor Mode is activated. passing request"); + } + + AddCredentialIntelligenceHeadersToRequest(application, config); + + PxLoggingUtils.LogDebug(string.Format("Valid request to {0}", application.Context.Request.RawUrl)); + PostPageRequestedActivity(pxContext); + } + else + { + PxLoggingUtils.LogDebug(string.Format("Invalid request to {0}", application.Context.Request.RawUrl)); + PostBlockActivity(pxContext, config); + } + + SetPxhdAndVid(pxContext); + // If implemented, run the customVerificationHandler. + if (customVerificationHandlerInstance != null) + { + customVerificationHandlerInstance.Handle(application, pxContext, config); + } + // No custom verification handler -> continue regular flow + else if (!verified && !pxContext.MonitorRequest) + { + BlockRequest(pxContext, config); + application.CompleteRequest(); + } + } + + private static void SetPxhdAndVid(PxContext pxContext) + { + + if (!string.IsNullOrEmpty(pxContext.Pxhd)) + { + string pxhd = PxConstants.COOKIE_PXHD_PREFIX + "=" + pxContext.Pxhd + "; path=/"; + pxContext.ApplicationContext.Response.AddHeader("Set-Cookie", pxhd); + } + } + + private void AddCredentialIntelligenceHeadersToRequest(HttpApplication application, PxModuleConfigurationSection config) + { + if (config.LoginCredentialsExtractionEnabled && pxContext.LoginCredentialsFields != null) + { + if (pxContext.IsBreachedAccount()) + { + application.Context.Request.Headers.Add(config.CompromisedCredentialsHeader, JSON.SerializeDynamic(pxContext.Pxde.breached_account, PxConstants.JSON_OPTIONS)); + } + + if (config.AdditionalS2SActivityHeaderEnabled) + { + Activity activityPayload = AdditionalS2SUtils.CreateAdditionalS2SActivity(config, null, null, pxContext); + application.Context.Request.Headers.Add("px-additional-activity", JSON.SerializeDynamic(activityPayload, new Options(prettyPrint: false, excludeNulls: false, includeInherited: true))); + application.Context.Request.Headers.Add("px-additional-activity-url", PxConstants.FormatBaseUri(config) + PxConstants.ACTIVITIES_API_PATH); + } + } + } + } +} diff --git a/PerimeterXModule/PxModuleConfigurationSection.cs b/PerimeterXModule/PxModuleConfigurationSection.cs index 639a8d0..5fdf5a3 100644 --- a/PerimeterXModule/PxModuleConfigurationSection.cs +++ b/PerimeterXModule/PxModuleConfigurationSection.cs @@ -579,5 +579,198 @@ public string ByPassMonitorHeader this["bypassMonitorHeader"] = value; } } + + [ConfigurationProperty("loginCredentialsExtractionEnabled", DefaultValue = false)] + public bool LoginCredentialsExtractionEnabled + { + get + { + return (bool)this["loginCredentialsExtractionEnabled"]; + } + + set + { + this["loginCredentialsExtractionEnabled"] = value; + } + } + + [ConfigurationProperty("loginCredentialsExtraction", DefaultValue = "")] + public string LoginCredentialsExtraction + { + get + { + return (string)this["loginCredentialsExtraction"]; + } + + set + { + this["loginCredentialsExtraction"] = value; + } + + } + + + [ConfigurationProperty("ciVersion", DefaultValue = "v2")] + public string CiVersion + { + get + { + return (string)this["ciVersion"]; + } + + set + { + this["ciVersion"] = value; + } + + } + + [ConfigurationProperty("compromisedCredentialsHeader", DefaultValue = "px-compromised-credentials")] + public string CompromisedCredentialsHeader + { + get + { + return (string)this["compromisedCredentialsHeader"]; + } + + set + { + this["compromisedCredentialsHeader"] = value; + } + + } + + [ConfigurationProperty("sendRawUsernameOnAdditionalS2SActivity", DefaultValue = false)] + public bool SendRawUsernameOnAdditionalS2SActivity + { + get + { + return (bool)this["sendRawUsernameOnAdditionalS2SActivity"]; + } + + set + { + this["sendRawUsernameOnAdditionalS2SActivity"] = value; + } + + } + + [ConfigurationProperty("additionalS2SActivityHeaderEnabled", DefaultValue = false)] + public bool AdditionalS2SActivityHeaderEnabled + { + get + { + return (bool)this["additionalS2SActivityHeaderEnabled"]; + } + + set + { + this["additionalS2SActivityHeaderEnabled"] = value; + } + + } + + + [ConfigurationProperty("loginSuccessfulReportingMethod", DefaultValue = "")] + public string LoginSuccessfulReportingMethod + { + get + { + return (string)this["loginSuccessfulReportingMethod"]; + } + + set + { + this["loginSuccessfulReportingMethod"] = value; + } + + } + + [ConfigurationProperty("loginSuccessfulBodyRegex", DefaultValue = "")] + public string LoginSuccessfulBodyRegex + { + get + { + return (string)this["loginSuccessfulBodyRegex"]; + } + + set + { + this["loginSuccessfulBodyRegex"] = value; + } + + } + + [ConfigurationProperty("loginSuccessfulHeaderName", DefaultValue = "")] + public string LoginSuccessfulHeaderName + { + get + { + return (string)this["loginSuccessfulHeaderName"]; + } + + set + { + this["loginSuccessfulHeaderName"] = value; + } + + } + + [ConfigurationProperty("loginSuccessfulHeaderValue", DefaultValue = "")] + public string LoginSuccessfulHeaderValue + { + get + { + return (string)this["loginSuccessfulHeaderValue"]; + } + + set + { + this["loginSuccessfulHeaderValue"] = value; + } + + } + + [ConfigurationProperty("loginSuccessfulStatus", DefaultValue = "")] + [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] + public StringCollection LoginSuccessfulStatus + { + get + { + return (StringCollection)this["loginSuccessfulStatus"]; + } + + set + { + this["loginSuccessfulStatus"] = value; + } + + } + + [ConfigurationProperty("customLoginSuccessfulHandler")] + public string CustomLoginSuccessfulHandler + { + get + { + return (string)this["customLoginSuccessfulHandler"]; + } + set + { + this["customLoginSuccessfulHandler"] = value; + } + } + + [ConfigurationProperty("customCredentialsExtractionHandler")] + public string CustomCredentialsExtractionHandler + { + get + { + return (string)this["customCredentialsExtractionHandler"]; + } + set + { + this["customCredentialsExtractionHandler"] = value; + } + } } } diff --git a/README.md b/README.md index 837d5ec..dabedfa 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [PerimeterX](http://www.perimeterx.com) ASP.NET SDK =================================================== -> Latest stable version: [v3.1.4](https://www.nuget.org/packages/PerimeterXModule/3.1.4) +> Latest stable version: [v3.2.0](https://www.nuget.org/packages/PerimeterXModule/3.2.0) Table of Contents ----------------- diff --git a/px_metadata.json b/px_metadata.json new file mode 100644 index 0000000..10bc77b --- /dev/null +++ b/px_metadata.json @@ -0,0 +1,32 @@ +{ + "version": "3.2.0", + "supported_features": [ + "additional_activity_handler", + "advanced_blocking_response", + "batched_activities", + "block_activity", + "block_page_captcha", + "bypass_monitor_header", + "client_ip_extraction", + "css_ref", + "custom_cookie_header", + "custom_logo", + "enforce_specific_routes", + "filter_by_extension", + "filter_by_route", + "first_party", + "js_ref", + "credentials_intelligence", + "mobile_support", + "module_enable", + "module_mode", + "page_requested_activity", + "pxde", + "pxhd", + "risk_api", + "sensitive_routes", + "sensitive_headers", + "telemetry_command", + "vid_extraction" + ] +} \ No newline at end of file