Sự khác biệt giữa HTML Encode và URL Encode trong .NET

Sự khác biệt giữa HTML Encode và URL Encode trong .NET

Là con người, chúng ta có khả năng bẩm sinh để chọn ra những thông tin có giá trị từ những tình huống không mấy lý tưởng. Bộ não của chúng ta có thể xử lý những thứ chúng ta cần trong khi bỏ qua những điều vô nghĩa. Thật không may, các thành phần chính của web như HTML và HTTP không có những khả năng tương tự và trên thực tế, không quá ngạc nhiên về điều này.

Là lập trình viên, chúng ta cần thực hiện các bước bổ sung khi truyền dữ liệu đến một trang HTML để đảm bảo rằng nó không phá vỡ các quy ước HTML. Ngoài ra, khi gửi yêu cầu đến máy chủ, chúng ta có thể cần phải tuân thủ các quy tắc HTTP để đảm bảo máy chủ không từ chối yêu cầu web của chúng ta.

Bài viết này sẽ giúp bạn tìm hiểu lớp WebUtility và phân tích cách các phương thức của nó làm việc với đầu vào của chúng ta để mã hóa / giải mã dữ liệu có khả năng bị phá vỡ.

Encode / Decode là gì?

Encode (Mã hóa) và Decode (giải mã) là hai mặt đối lập của cùng một quá trình: lấy đầu vào của người dùng và làm sạch nó cho người nhận dự định.

Phương pháp encode nhận một giá trị mà con người có thể đọc được và chuyển đổi thành các ký tự có thể gây trở ngại cho người nhận. Trong trường hợp HTML, những ký tự đó có thể là dấu ngoặc nhọn (<>).

Quên mã hóa thông tin đầu vào của người dùng là một vấn đề điển hình đối với các cuộc tấn công xss (cross-site scripting) trên nhiều trang web.

Người dùng có thể nhập dữ liệu vào ứng dụng và trang web hiển thị dữ liệu đó mà không cần xác nhận tính an toàn của nó trước. Chúng ta sẽ đi vào chi tiết hơn trong phần HTML Encode của bài viết này.

Quá trình decode đảo ngược quá trình encode. Quá trình này lấy các ký tự đã được thay thế và hoàn nguyên chúng về giá trị ban đầu. Việc decode có thể hữu ích trong các tình huống mà giá trị ban đầu cần được xác minh bằng mắt người nhưng cuối cùng lại được gửi đến người nhận không phải là con người.

Chúng ta có thể đã thấy các URL được mã hóa với các giá trị như %20, cho biết một khoảng trống trong URL của chúng ta. Một con người có thể cảm thấy khó đọc Hello%2C+World, nhưng giá trị ban đầu của nó là Hello, World rất tự nhiên.

Chúng ta hãy đi sâu vào các chi tiết cụ thể về HTML Encode và URL Encode và cả hai khác nhau như thế nào ở phần tiếp theo.

Các lớp tiện ích WebUtility và HttpUtility

.NET Runtime có một lớp WebUtility chứa các phương thức an toàn nhất trong .NET để thực hiện các tác vụ encode cốt lõi được nhắm mục tiêu trên web. Nó là một phần của namepsace System.Net và có thể được tìm thấy trong assembly System.Runtime.

Ngoài ra còn có lớp HttpUtility được tìm thấy ở namespace System.Web.Util có thể thực hiện thêm encode / decode trên thuộc tính HTML, chuỗi truy vấn, JavaScript, và các hoạt động khác sử dụng các kiểu Encoding, trong đó bao gồm các biến thể của UTF8, UTF16Unicode.

Vì lợi ích của bài viết này, chúng ta sẽ bám sát các triển khai của lớp WebUtility.

Url Encode

Chúng ta hãy xem phương thức UrlEncodeđược tìm thấy trong lớp WebUtility. Trong phương thức này, chúng ta sẽ xem những ký tự nào được coi là an toàn và những ký tự nào cần được thay đổi để tôn trọng người nhận URL của chúng ta.

public static string? UrlEncode(string? value)
{
    if (string.IsNullOrEmpty(value))
        return value;

    int safeCount = 0;
    int spaceCount = 0;
    for (int i = 0; i < value.Length; i++)
    {
        char ch = value[i];
        if (IsUrlSafeChar(ch))
        {
            safeCount++;
        }
        else if (ch == ' ')
        {
            spaceCount++;
        }
    }

    int unexpandedCount = safeCount + spaceCount;
    if (unexpandedCount == value.Length)
    {
        if (spaceCount != 0)
        {
            // Only spaces to encode
            return value.Replace(' ', '+');
        }

        // Nothing to expand
        return value;
    }

    int byteCount = Encoding.UTF8.GetByteCount(value);
    int unsafeByteCount = byteCount - unexpandedCount;
    int byteIndex = unsafeByteCount * 2;

    // Instead of allocating one array of length `byteCount` to store
    // the UTF-8 encoded bytes, and then a second array of length
    // `3 * byteCount - 2 * unexpandedCount`
    // to store the URL-encoded UTF-8 bytes, we allocate a single array of
    // the latter and encode the data in place, saving the first allocation.
    // We store the UTF-8 bytes to the end of this array, and then URL encode to the
    // beginning of the array.
    byte[] newBytes = new byte[byteCount + byteIndex];
    Encoding.UTF8.GetBytes(value, 0, value.Length, newBytes, byteIndex);

    GetEncodedBytes(newBytes, byteIndex, byteCount, newBytes);
    return Encoding.UTF8.GetString(newBytes);
}

Phần đáng chú ý nhất của phương thức là lời gọi đến phương thức IsUrlSafeChar. Các giá trị mà chúng ta có thể thêm vào URL một cách an toàn là gì? Nhìn vào phương thức này, chúng ta có thể thấy một cách triển khai chưa được tối ưu hóa.

if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9')
{
    return true;
}

switch (ch)
{
    case '-':
    case '_':
    case '.':
    case '!':
    case '*':
    case '(':
    case ')':
        return true;
}

return false;

Hóa ra tất cả mọi thứ ngoại trừ chữ và số ký tự, và các ký tự -, _, ., !, *, ()là không an toàn. Quay trở lại phương thức ban đầu UrlEncode, chúng ta có thể thấy điều gì sẽ xảy ra với tất cả các giá trị không mong muốn đó.

  1. Đầu tiên, phương thức chuyển đổi các giá trị khoảng trắng thành ký tự dấu cộng (+).
  2. Cuối cùng, phương thức chuyển đổi các giá trị còn lại thành byte tương đương của chúng và sau đó nhận giá trị chuỗi. Mã hóa đạt được bằng cách sử dụng các phương thức Encoding.UTF8.GetBytesEncoding.UTF8.GetString.

Bây giờ chúng ta hãy xem HTML Encode và xem nó khác với URL Encode như thế nào.

HTML Encode

Cũng ở trong lớp WebUitlity, chúng ta sẽ tìm thấy phương thức HtmlEncode. Chúng ta sử dụng phương thức này để nhận các giá trị mà chúng ta muốn hiển thị trong HTML hiện có nhưng không muốn đầu vào của chúng ta làm hỏng cấu trúc HTML. Hãy xem cách .NET thực hiện phương thức này.

public static void HtmlEncode(string? value, TextWriter output)
{
    if (output == null)
    {
        throw new ArgumentNullException(nameof(output));
    }
    if (string.IsNullOrEmpty(value))
    {
        output.Write(value);
        return;
    }

    ReadOnlySpan<char> valueSpan = value.AsSpan();

    // Don't create ValueStringBuilder if we don't have anything to encode
    int index = IndexOfHtmlEncodingChars(valueSpan);
    if (index == -1)
    {
        output.Write(value);
        return;
    }

    // For small inputs we allocate on the stack. In most cases a buffer three
    // times larger the original string should be sufficient as usually not all
    // characters need to be encoded.
    // For larger string we rent the input string's length plus a fixed
    // conservative amount of chars from the ArrayPool.
    ValueStringBuilder sb = value.Length < 80 ?
        new ValueStringBuilder(stackalloc char[256]) :
        new ValueStringBuilder(value.Length + 200);

    sb.Append(valueSpan.Slice(0, index));
    HtmlEncode(valueSpan.Slice(index), ref sb);

    output.Write(sb.AsSpan());
    sb.Dispose();
}

Chúng ta có thể tìm thấy phần thú vị của phương thức HtmlEncode chính là lệnh gọi tới phương thức IndexOfHtmlEncodingChars.

private static int IndexOfHtmlEncodingChars(ReadOnlySpan<char> input)
{
    for (int i = 0; i < input.Length; i++)
    {
        char ch = input[i];
        if (ch <= '>')
        {
            switch (ch)
            {
                case '<':
                case '>':
                case '"':
                case ''':
                case '&':
                    return i;
            }
        }
#if ENTITY_ENCODE_HIGH_ASCII_CHARS
        else if (ch >= 160 && ch < 256)
        {
            return i;
        }
#endif // ENTITY_ENCODE_HIGH_ASCII_CHARS
        else if (char.IsSurrogate(ch))
        {
            return i;
        }
    }

    return -1;
}

Quá trình HTML Encode đảm bảo rằng các ký tự <, >, ", &được gắn cờ phải được thay thế bởi các ký tự HTML thân thiện với chúng. Giá trị nào thay thế các ký tự này? Chúng ta có thể tìm thấy điều đó trong một phương thức HtmlEncode.

switch (ch)
{
    case '<':
        output.Append("<");
        break;
    case '>':
        output.Append(">");
        break;
    case '"':
        output.Append(""");
        break;
    case ''':
        output.Append("'");
        break;
    case '&':
        output.Append("&");
        break;
    default:
        output.Append(ch);
        break;
}

Các giá trị được HTML Encode sẽ bắt đầu bằng dấu và (&) và kết thúc bằng dấu chấm phẩy (;).

Phần kết luận

Như chúng ta đã thấy trong việc triển khai URL Encode và HTML Encode, chúng đều thực hiện mục tiêu cuối cùng là thay đổi giá trị để an toàn cho người nhận.

Trong HTML Encode, chúng ta thay đổi các ký tự có khả năng phá vỡ cấu trúc của trang HTML hiện có để khách hàng có thể hiển thị chúng một cách an toàn.

Trong URL Encode, chúng ta thay đổi các giá trị có thể vi phạm tính liên tục của URL, khiến người nhận hiểu sai giá trị URL đầy đủ.

Nếu bạn đang tìm thấy các hành vi lạ trong các trang ASP.NET được hiển thị của mình, bạn có thể muốn kiểm tra xem bạn có đang mã hóa các giá trị đúng cách hay không. May mắn thay, người dùng ASP.NET nhận được mã hóa tự động khi sử dụng Razor, vì vậy đây không phải là vấn đề phổ biến mà là điều cần lưu ý.

Nếu máy chủ từ chối yêu cầu của bạn, có thể có các giá trị trong URL của bạn đang phá vỡ tính hoàn chỉnh của nó. Kiểm tra các ký tự được đề cập ở trên và đảm bảo mã hóa URL của bạn trước khi thực hiện yêu cầu của bạn.

Tôi hy vọng bạn thích bài viết này và như mọi khi, cảm ơn bạn vì đã đọc.

Trả lời

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 *