Giải mã chuỗi truy vấn trong ASP.NET Core MVC bằng Action Filter

Giải mã chuỗi truy vấn trong ASP.NET Core MVC bằng Action Filter

Gần đây tôi đã có thời gian làm việc với một nhà phát triển cần hỗ trợ một định dạng bảo mật cũ. Một ứng dụng cũ đã mã hóa tất cả các giá trị vào trong một tham số chuỗi truy vấn duy nhất, sau đó thực hiện một yêu cầu HTTP đến một điểm cuối ASP.NET Core MVC.

Bài viết này sẽ trình bày cách chúng ta có thể tận dụng Action Filter (bộ lọc hành động) trong ASP.NET Core MVC để làm cho trải nghiệm phát triển trong các action method (phương thức hành động) của chúng ta giống với trải nghiệm mà chúng ta đã sử dụng suốt thời gian qua.

Vấn đề

Các hệ thống cũ có thể không có quyền truy cập vào tất cả các giao thức và mẫu bảo mật mà chúng ta có ngày nay, vì vậy chúng có thể phải thực hiện một số giải pháp giải quyết vấn đề sáng tạo.

Trong trường hợp của chúng ta, chúng ta có một tham số chuỗi truy vấn là secret được mã hóa bằng TripleDES và chúng ta mong đợi người nhận giải mã nó để lấy ra các giá trị.

/?secret=1w58y%2BC60jon25f%2F4VvVHUOX%2FIxs%2FEVx

Kỹ thuật này có thể xảy ra vì các thông tin bí mật trong URL có thể được ghi vào trong log. Dù lý do là gì, chúng ta có một vấn đề để giải quyết.

Endpoint của chúng ta sẽ có hai tham số numbername chứ không phải là secret vàchúng ta mong đợi hai tham số number và name sẽ nhận được giá trị khi dữ liệu được gửi tới endpoint.

[Route(""), HttpPost]
public IActionResult IndexPost(int number, string name)
{
    return View("Index",
        new IndexModel
        {
            Number = number,
            Name = name
        });
}

Chúng ta có một giải pháp đơn giản cho vấn đề này và nó liên quan đến Action Filter.

Giải pháp sử dụng Action FilterAction Argument

Action Filter (Bộ lọc hành động) là một cách tuyệt vời để chặn các yêu cầu đến và bổ sung các quy trình thực thi. Trong trường hợp của chúng ta, chúng ta muốn chuyển đổi tham số chuỗi truy vấn hiện có (secret), giải mã các giá trị của nó và gán giá trị đó cho các tham số numbername của phương thức hành động (action method).

Cuối cùng chúng ta sẽ sử dụng một thuộc tính EncryptedParametersAttribute được triển khai từ ActionFilterAttribute để gắn trên các phương thức hành động.

[Route(""), HttpPost]
[EncryptedParameters("secret")]
public IActionResult IndexPost(int number, string name)
{
    return View("Index",
        new IndexModel
        {
            Number = number,
            Name = name
        });
}

Hãy xem mã triển khai attribute EncryptedParametersAttribute.

using System;
using System.Globalization;
using System.Linq;
using System.Web;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Secrets.Models;

namespace Secrets.Controllers
{
    public class EncryptedParametersAttribute : ActionFilterAttribute
    {
        public string ParameterName { get; }

        public EncryptedParametersAttribute(string parameterName = "secret")
        {
            ParameterName = parameterName;
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            var config = context.HttpContext.RequestServices.GetRequiredService<IOptions<CryptoEngine.Secrets>>();
            var encrypted = context.HttpContext.Request.Query[ParameterName].FirstOrDefault();

            // decrypt secret
            var decrypted = CryptoEngine.Decrypt(encrypted, config.Value.Key);
            var collection = HttpUtility.ParseQueryString(decrypted);
            var actionParameters = context.ActionDescriptor.Parameters;

            foreach (var parameter in actionParameters)
            {
                try
                {
                    var value = collection[parameter.Name];

                    if (value == null)
                        continue;

                    // set the action arguments to the values 
                    // from the encrypted parameter
                    context.ActionArguments[parameter.Name] =
                        ConvertToType(value, parameter.ParameterType);
                }
                catch (Exception e)
                {
                    context.ModelState.TryAddModelException(parameter.Name, e);
                }
            }
        }

        private static object? ConvertToType(string value, Type type)
        {
            var underlyingType = Nullable.GetUnderlyingType(type);

            if (value.Length > 0)
            {
                if (type == typeof(DateTimeOffset) || underlyingType == typeof(DateTimeOffset))
                {
                    return DateTimeOffset.Parse(value, CultureInfo.InvariantCulture);
                }

                if (type == typeof(DateTime) || underlyingType == typeof(DateTime))
                {
                    return DateTime.Parse(value, CultureInfo.InvariantCulture);
                }

                if (type == typeof(Guid) || underlyingType == typeof(Guid))
                {
                    return new Guid(value);
                }

                if (type == typeof(Uri) || underlyingType == typeof(Uri))
                {
                    if (Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var uri))
                    {
                        return uri;
                    }

                    return null;
                }
            }
            else
            {
                if (type == typeof(Guid))
                {
                    return default(Guid);
                }

                if (underlyingType != null)
                {
                    return null;
                }
            }

            if (underlyingType is not null)
            {
                return Convert.ChangeType(value, underlyingType);
            }

            return Convert.ChangeType(value, type);
        }
    }
}

Bằng cách sử dụng phương thức ActionExecutingContext, chúng ta có quyền truy cập vào các tham số của điểm cuối mục tiêu của chúng ta. Sau đó, vấn đề chỉ là giải mã dữ liệu trong tham số truy vấn secret của chúng ta, và gán giá trị cho các tham số của phương thức hành động bằng cách sử dụng các tham số đã biết của chúng ta.

public override void OnActionExecuting(ActionExecutingContext context)
{
    var config = context.HttpContext.RequestServices.GetRequiredService<IOptions<CryptoEngine.Secrets>>();
    var encrypted = context.HttpContext.Request.Query[ParameterName].FirstOrDefault();

    // decrypt secret
    var decrypted = CryptoEngine.Decrypt(encrypted, config.Value.Key);
    var collection = HttpUtility.ParseQueryString(decrypted);
    var actionParameters = context.ActionDescriptor.Parameters;

    foreach (var parameter in actionParameters)
    {
        try
        {
            var value = collection[parameter.Name];

            if (value == null)
                continue;

            // set the action arguments to the values 
            // from the encrypted parameter
            context.ActionArguments[parameter.Name] =
                ConvertToType(value, parameter.ParameterType);
        }
        catch (Exception e)
        {
            context.ModelState.TryAddModelException(parameter.Name, e);
        }
    }
}

Việc chuyển đổi các kiểu dữ liệu trong ví dụ trên của chúng ta chỉ xử lý các kiểu nguyên thủy và kiểu nullable nhưng không xử lý các kiểu dữ liệu phức tạp. Vui lòng sửa đổi mã này để đáp ứng nhu cầu cụ thể của bạn.

Đối với những người quan tâm đến lớp mã hóa CryptoEngine, đây là nó, nhưng tôi không coi mình là một chuyên gia mã hóa.

using System;
using System.Security.Cryptography;
using System.Text;

namespace Secrets.Models
{
    /// <summary>
    /// modified from the following post
    /// https://dotnetcodr.com/2015/10/23/encrypt-and-decrypt-plain-string-with-triple-des-in-c/
    /// </summary>
    public static class CryptoEngine
    {
        public class Secrets
        {
            public string Key { get; set; }
        }

        public static string Encrypt(string source, string key)
        {
            var byteHash = MD5.HashData(Encoding.UTF8.GetBytes(key));
            var tripleDes = new TripleDESCryptoServiceProvider
            {
                Key = byteHash, 
                Mode = CipherMode.ECB
            };
            
            var byteBuff = Encoding.UTF8.GetBytes(source);
            return Convert.ToBase64String(tripleDes.CreateEncryptor()
                .TransformFinalBlock(byteBuff, 0, byteBuff.Length));
        }

        public static string Decrypt(string encodedText, string key)
        {
            var byteHash = MD5.HashData(Encoding.UTF8.GetBytes(key));
            var tripleDes = new TripleDESCryptoServiceProvider
            {
                Key = byteHash, 
                Mode = CipherMode.ECB
            };
            var byteBuff = Convert.FromBase64String(encodedText);
            return Encoding.UTF8.GetString(
                tripleDes
                    .CreateDecryptor()
                    .TransformFinalBlock(byteBuff, 0, byteBuff.Length));
        }
    }
}

Trong thực tế

Tôi đã tạo một ứng dụng ASP.NET Core MVC đơn giản để chứng minh bộ lọc hành động này đang được sử dụng. Bắt đầu với view, chúng ta sẽ mã hóa một số giá trị và gửi chúng lên điểm cuối sử dụng attribute EncryptedParametersAttribute của chúng ta.

@model IndexModel
@inject Microsoft.Extensions.Options.IOptions<CryptoEngine.Secrets> config
@{
    ViewData["Title"] = "Home Page";
    var values = "name=Khalid&number=57";
    var secret = CryptoEngine.Encrypt(values, config.Value.Key);
    var url = Url.Action("IndexPost", new {secret});
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>

    <p>Click this link to transmit secret values via Querystring</p>
    <form action="@url" method="POST">
        <p>@url</p>
        <button type="submit">Submit</button>
    </form>
</div>

@if (Model != null)
{
    <section style="margin-top: 1em">
        <div class="text-center">
            <h2>Secrets</h2>
            <div>
                <label asp-for="Name"></label>
                @Model.Name
            </div>
            <div>
                <label asp-for="Number"></label>
                @Model.Number
            </div>
        </div>
    </section>
}

Khi chúng ta gửi biểu mẫu, chúng ta sẽ nhận được dữ liệu được mã hóa trong tham số truy vấn secret, attribute EncryptedParametersAttribute sẽ tiến hành giải mã chúng, gắn dữ liệu cho tham số number, name và hiển thị lên trang.

Phần kết luận

Chúng ta có thể sử dụng kỹ thuật này để chuyển đổi bất kỳ chuỗi truy vấn đến thành bất kỳ tập hợp tham số nào khác. Kỹ thuật này mạnh mẽ và có thể được áp dụng cho bất kỳ phương thức hành động nào của MVC hoặc có thể được áp dụng toàn cục.

Như thường lệ, hãy sử dụng mã này làm điểm bắt đầu và thay đổi mã để đáp ứng nhu cầu cụ thể của bạn.

Cảm ơn bạn đã giành thời gian đọc bài viết này!

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *