Base solution for your next web application

Activities of "hongbing.wang"

namespace umsplus.Web.Xss { using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; using System.Collections.Generic; using System.Reflection;

public class SanitizeInputFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        var htmlSanitizer = context.HttpContext.RequestServices.GetService<IHtmlSanitizer>();
        foreach (var argument in context.ActionArguments.Values)
        {
            SanitizeObjectProperties(argument, htmlSanitizer);
        }
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // No need to do anything here
    }

    private void SanitizeObjectProperties(object obj, IHtmlSanitizer sanitizer, HashSet<object> processedObjects = null)
    {
        if (obj == null) return;

        // Initialize the HashSet if it's the first call
        if (processedObjects == null)
        {
            processedObjects = new HashSet<object>();
        }

        // Check if the object has already been processed to avoid infinite recursion
        if (processedObjects.Contains(obj))
        {
            return;
        }

        // Add the current object to the set of processed objects
        processedObjects.Add(obj);

        var properties = obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
        foreach (var property in properties)
        {
            // Skip properties that require index parameters (like indexers)
            if (property.GetIndexParameters().Length > 0)
            {
                continue;
            }

            if (property.PropertyType == typeof(string) && property.CanWrite)
            {
                var value = (string)property.GetValue(obj);
                if (value != null)
                {
                    var sanitizedValue = sanitizer.Sanitize(value);
                    property.SetValue(obj, sanitizedValue);
                }
            }
            else if (!property.PropertyType.IsPrimitive && !property.PropertyType.IsEnum && property.PropertyType != typeof(string))
            {
                var propertyValue = property.GetValue(obj);
                if (propertyValue != null)
                {
                    SanitizeObjectProperties(propertyValue, sanitizer, processedObjects);
                }
            }
        }
    }
}

}

In the original ASP.NET Zero, the HTML sanitizer config: Configuration.Modules.AbpHtmlSanitizer() .KeepChildNodes() .AddSelector<IAccountAppService>(x => nameof(x.IsTenantAvailable)) .AddSelector<IAccountAppService>(x => nameof(x.Register));

We have extended to include all the API methods except a couple. It seems OK. It seems there are no issues without KeepChildNodes(),

Questions:

  1. Does Abp.HtmlSanitizer cover all required for sanitizing XSS? Do we need to do additional input filtering?
  2. ShouldSanitizeContext is not exposed. How is it configured?
  3. Any risks to filter all the API methods?

We have implemented a global input filter SanitizeInputFilter using the same Abp.HtmlSanitizer. Our test results:

  • SanitizeInputFilter: Input: "20240705 <a href=></a>", output: "20240705". It filters out HTML code.
  • options.Filters.AddService(typeof(AbpHtmlSanitizerActionFilter)): Input: "20240705 <a href=></a>", Output: "20240705 <a href=></a>". It doesn't filter out HTML code.

Why SanitizeInputFilter and AbpHtmlSanitizerActionFilter have different filtering behavior? Should "<a href=></a>" be filtered out by AbpHtmlSanitizerActionFilter or just be sanitized? Is the discrepancy caused by the flag ShouldSanitizeContext with AbpHtmlSanitizerActionFilter?

Answer

I've figured out the issue with using use ASP.NET Core Session. In startup.cs, I also need to add the following service registration.

services.AddSession(options => { options.IdleTimeout = TimeSpan.FromSeconds(30); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; });

When logging in, the HttpContext.Session has the two key-value pairs. However in Invoke(HttpContext context) in the validator, context.Session lost the key-value pairs. So it is still not working.

Developer tool, Application tab > Under Storage, select Cookies and then the site (http://localhost:4200), there is not any cookie named .AspNetCore.Session or something similar.

Answer

Hi @ismcagdas,

It doesn't.

So, I want to use ASP.NET Core Session instead of AbpSession.

I aim to use HttpContext.Session.SetString("SId", sessionId); HttpContext.Session.SetString("UId", loginResult.User.Id.ToString());

In Startup.cs, I added app.UseSession()

But it throws an exception at app.UseAbp(options => { options.UseAbpRequestLocalization = false; //used below: UseAbpRequestLocalization }); The exception is Component Abp.Domain.Uow.UnitOfWorkDefaultOptions could not be registered. There is already a component with that name. Did you want to modify the existing component instead? If not, make sure you specify a unique name.

How do I resolve the error? Can I use ASP.NET Core Session anyway?

Answer

Hi @ismcagdas,

I've added SessionValidationMiddleware, seesionID claim in access and refresh tokens, and UserSessionCache with seesionID and userId. See the code below.

If an admin's token is copied to a non-admin user. jwtUserIdClaimed is the admin userId, but userId is also changed to admin userId / jwtUserIdClaimed?

Does this mean that the userId in _abpSession is always the same as jwtUserIdClaimed. Is the principal synced with the access token by ABP?

Please reproduce this session hijacking issue with ASP.NET Zero 13.1.0 code. We use ASP.NET Zero 13.1.0.

This is a critical security issue. A hacker can copy an admin user's access token through developer tool and make him gain admin privileges. Please investigate and resolve the issue.

` public class SessionValidationMiddleware { private readonly RequestDelegate _next; private readonly ICacheManager _cacheManager; private readonly IAbpSession _abpSession;

    public SessionValidationMiddleware(RequestDelegate next, ICacheManager cacheManager, IAbpSession abpSession)
    {
        _next = next;
        _cacheManager = cacheManager;
        _abpSession = abpSession;
    }

    public async Task Invoke(HttpContext context)
    {
        var userClaims = context.User.Claims;
        var sessionIdClaim = userClaims.FirstOrDefault(c => c.Type == "SessionId");
        var userIdClaim = userClaims.FirstOrDefault(c => c.Type == "user_identifier");

        if (sessionIdClaim != null && userIdClaim != null)
        {
            if (userIdClaim.Value != _abpSession.ToUserIdentifier().ToUserIdentifierString())
            {
                //never reached here. They should be the same.
                throw new WmsproException((int)HttpStatusCode.Unauthorized, "Invalid user identifier");
            }

            //user_identifier: UserId + "@" + TenantId
            var parts = userIdClaim.Value.Split('@');
            if (parts.Length == 2)
            {
                var userId = parts[0];
                //var tenantId = parts[1];

                if (userId != _abpSession.ToUserIdentifier().UserId.ToString())
                {
                    //never reached here. They should be the same.
                    throw new WmsproException((int)HttpStatusCode.Unauthorized, "Invalid user identifier");
                }

                var sessionId = sessionIdClaim.Value;

                var token = context.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");

                if (!string.IsNullOrEmpty(token))
                {
                    var handler = new JwtSecurityTokenHandler();
                    var jwtToken = handler.ReadToken(token) as JwtSecurityToken;

                    if (jwtToken != null)
                    {
                        var jwtUserIdClaim = jwtToken.Claims.FirstOrDefault(claim => claim.Type == JwtRegisteredClaimNames.Sub);
                        var jwtSessionIdClaim = jwtToken.Claims.FirstOrDefault(claim => claim.Type == "SessionId");

                        if (jwtUserIdClaim != null && jwtSessionIdClaim != null)
                        {
                            var jwtUserIdClaimed = jwtUserIdClaim.Value;
                            //seems _abpSession always gets the userId from jwt access token rather than from the principal, or the principal is synced with the token by ABP. 
                            //var userId = _abpSession.UserId.Value.ToString();
                            var jwtSessionIdClaimed = jwtSessionIdClaim.Value;

                            // Retrieve the cached session ID using the correct method signature
                            var cache = _cacheManager.GetCache<string, string>("UserSessionCache");
                            var cachedSessionId = await cache.GetAsync(userId, async (key) => null); // Assuming a factory function returning null if not found

                            // Retrieve the current session user ID (assumes session is available)
                            //var sessionUserId = context.Session.GetString("UserId");

                            //If an admin's token is copied to a non-admin. jwtUserIdClaimed is the admin userId, but userId is also changed to admin userId / jwtUserIdClaimed?
                            //This means that the userId in _abpSession is always the same as jwtUserIdClaimed. Is the principal synced with the access token by ABP?
                            if ((cachedSessionId != null && (cachedSessionId != sessionId || cachedSessionId != jwtSessionIdClaimed)) ||
                                (jwtSessionIdClaimed != sessionId || jwtUserIdClaimed != userId))
                            {
                                throw new WmsproException((int)HttpStatusCode.Unauthorized, "Invalid session");
                            }
                        }
                        else
                        {
                            //context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                            //await context.Response.WriteAsync("Token is missing required claims.");
                            //return;
                        }
                    }
                }
            }
        }

        await _next(context);
    }
}`

In Startup.cs, I have added app.UseMiddleware<SessionValidationMiddleware>();

The changes in TokenAuthController.cs: InternalAuthenticate(): ` var sessionId = Guid.NewGuid().ToString(); await _cacheManager.GetCache("UserSessionCache").SetAsync(loginResult.User.Id.ToString(), sessionId, TimeSpan.FromMinutes(30));

        var refreshToken = CreateRefreshToken(await CreateJwtClaims(loginResult.Identity, loginResult.User,
            tokenType: TokenType.RefreshToken, sessionId: sessionId));

        var accessToken = CreateAccessToken(await CreateJwtClaims(loginResult.Identity, loginResult.User,
            refreshTokenKey: refreshToken.key, sessionId: sessionId));`

` private async Task<IEnumerable

        if (_identityOptions.ClaimsIdentity.UserIdClaimType != JwtRegisteredClaimNames.Sub)
        {
            claims.Add(new Claim(JwtRegisteredClaimNames.Sub, nameIdClaim.Value));
        }

        if (!string.IsNullOrEmpty(sessionId))
        {
            claims.AddRange(new[]
            {
                new Claim("SessionId", sessionId),
            });
        }

        claims.AddRange(new[]
        {
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.Now.ToUnixTimeSeconds().ToString(),
                ClaimValueTypes.Integer64),
            new Claim(AppConsts.TokenValidityKey, tokenValidityKey),
            new Claim(AppConsts.UserIdentifier, user.ToUserIdentifier().ToUserIdentifierString()),
            new Claim(AppConsts.TokenType, tokenType.To<int>().ToString())
        });     `       
Answer

Hi @ismcagdas, Not sure whether forcing anti-forgery validation will fix the session hijacking issue. Put anti-forgery validation aside, have you considered the session hijacking issue in the original Zero app. Please note that I can reproduce it in ASP.NET Zero 12.4.2.

Answer

Hi @ismcagdas,

Yes, our app has HTTP-Only cookies enabled. The HttpOnly flag prevents client-side scripts (JavaScript) from accessing the cookie containing the token. This helps mitigate Cross-site Scripting (XSS) attacks, where an attacker injects malicious code into a web page and steals the token using JavaScript.

However, it doesn't prevent access from the developer tools provided by your browser. Anyone with access to your browser, including yourself, can view and copy the token using these tools.

Please try Zero app 12.4.2: copy the Abp.AuthToken from an admin user to a non-admin user. The non-admin user now has the admin rights.

Answer

Hi @ismcagdas, Yes, we host Angular app with the Host app together under the same domain.

Answer

I have reinstated [ValidateAntiForgeryToken] attributes. But it didn't help solve the reported session hijacking described at the top pf this post.

The example of session hijacking: I copied the AbP.AuthToken from an admin user to a non-admin user. The non-admin user now has the admin rights, which is an authentication and authorization issue.

I think the [ValidateAntiForgeryToken] attribute won't directly address the session hijacking. It is not directly applicable to preventing token impersonation. Is this an authentication issue?

Please note that this issue can also be reproduced with Zero app 12.4.2.

Hi @ismcagdas, Thank you for your suggestion. Please have a look at my implementation of ApiDocController below. Am I on the right track? I can see that the attribute routing works for the existing TokenAuthController. I think in this case I don't need to do anything special to enable attribute routing, right? I haven't got it working. Please point out what is missed. Thank you.

`namespace umsplus.Web.Controllers { [Route("apidoc")] public class ApiDocController : umsplusControllerBase { public ApiDocController() {

    }

    [HttpGet("{fileName}")]
    public IActionResult GetFile(string fileName)
    {
        // Assuming your apidoc files are stored in the wwwroot/apidoc directory
        // Validate and sanitize fileName
        fileName = Path.GetFileNameWithoutExtension(fileName) + Path.GetExtension(fileName); // Remove potential path traversal attempts

        // Enforce directory restriction
        var filePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "apidoc", fileName);
        filePath = Path.GetFullPath(filePath); // Normalize path

        if (filePath.StartsWith(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "apidoc")))
        {
            // Proceed with file retrieval if path is valid
            if (System.IO.File.Exists(filePath))
            {
                var fileContent = System.IO.File.ReadAllBytes(filePath);
                return File(fileContent, "application/octet-stream", fileName);
            }
            else
            {
                return NotFound();
            }
        }
        else
        {
            // Handle invalid path attempts, potentially logging or raising an alert
            return BadRequest("Invalid file path");
        }
    }
}

}`

Showing 1 to 10 of 36 entries