WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

文字列の長さ制限、三点リーダー表示

by WebSurfer 23. June 2023 13:52

先の記事「GridView に overflow 適用、三点リーダー表示」で、CSS の overflow: hidden と text-overflow: ellipsis を使って文字列の長さ制限するとともに三点リーダーを表示する例を紹介しましたが、文字列の中にエスケープされた文字、プロポーショナルフォント、サロゲートペア、絵文字などが含まれる場合どうなるかという話を書きます。

文字列の長さ制限、三点リーダー表示

上の画像の下側の表の description 列の各行が、文字列の長さを 320px に制限するとともに末尾を三点リーダーで表示したものです。具体的にどのようにしたかと言うと、上側の表の description 列と同じ文字列を div 要素に入れて、その div 要素に以下の CSS を適用しました。ブラウザは Chrome、フォントはメイリオ、サイズは 16px です。

<style type="text/css">
    div.style1
    {
        width: 320px;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }
</style>

1 行目にある < > & という文字は、実際は &lt; &gt; &amp; というエスケープされた文字列が、ブラウザ上では < > & と表示されたものです。

4 行目の絵文字 🍎 と 🍏 は UTF-16 のサロゲートペア (🍎 は 0xD83C 0xDF4E, 🍏 は 0xD83C 0xDF4F) です。👨‍🌾 は、👨 と 🌾 (おのおのサロゲートペアで 0xD83D 0xDC68 と 0xD83C 0xDF3E) を ZeroWidthJoiner (0x200D) で連結したもの、つまり、0xD83D 0xDC68 0x200D 0xD83C 0xDF3E となっています。

上の結果から分かるように、各文字の長さやバイト数とは関係なく、ブラウザ上に表示された文字列の長さで制限がかかり、CSS の width: 320px で指定された幅いっぱいに三点リーダを含めて表示されています。

上の画像ではフォントはメイリオ、サイズは 16px ですが、MS Gothic などの等幅フォントを使った場合も、フォントサイズを変えた場合も、ブラウザ上に表示される文字列の長さで制限がかかるのは同じです。

三点リーダーを表示する text-overflow:ellipsis はもともと IE の独自拡張だそうですが、最近は他のブラウザでも取り入れられているようです。Windows 10 で Chrome 114.0.5735.134, Edge 114.0.1823.58, Firefox 114.0.2, Opera 100.0.4815.21 で試してみましたが、同じ結果が得られました。

参考に、上の画像を表示するのに使った ASP.NET Web Forms アプリのコードを載せておきます。

.aspx.cs

using System;
using System.Data;

namespace WebForms1
{
    public partial class WebForm26 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                // データソースとして DataTable を作成。
                DataTable dt = new DataTable();
                DataRow dr;

                dt.Columns.Add(new DataColumn("id", typeof(Int32)));
                dt.Columns.Add(
                    new DataColumn("description", typeof(string)));

                dr = dt.NewRow();
                dr["id"] = 1;
                dr["description"] = "エスケープされた < > & " +
                    "などの文字はどのようになるか?";
                dt.Rows.Add(dr);

                dr = dt.NewRow();
                dr["id"] = 2;
                dr["description"] = "Proportional Font WWWWWWWWWWW " +
                    "iiiiiiii llllll などは?";
                dt.Rows.Add(dr);

                dr = dt.NewRow();
                dr["id"] = 3;
                dr["description"] = "サロゲートペア 𠀋 𡈽 𠮟 などは" +
                    "どのようになるか?";
                dt.Rows.Add(dr);

                dr = dt.NewRow();
                dr["id"] = 4;
                dr["description"] = "絵文字 🍎 🍏 (サロゲートペア) " +
                    "👨‍🌾 (ZWJ で結合) などは?";
                dt.Rows.Add(dr);

                // 上で作成した DataTable を GridView にバインド。
                GridView1.DataSource = dt;
                GridView1.DataBind();

                GridView2.DataSource = dt;
                GridView2.DataBind();
            }
        }
    }
}

.aspx

<%@ Page Language="C#" AutoEventWireup="true"
    CodeBehind="WebForm26.aspx.cs" Inherits="WebForms1.WebForm26" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
    <style type="text/css">
        body {
            font-family: "メイリオ";
            font-size: 16px;
        }

        div.style1 {
            width: 320px;
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
        <p>制限しない場合 (フォント: メイリオ, 16px)</p>
        <asp:GridView ID="GridView1"
            runat="server"
            AutoGenerateColumns="False">
            <Columns>
                <asp:BoundField DataField="id" HeaderText="id" />
                <asp:TemplateField HeaderText="description">
                    <ItemTemplate>
                        <asp:Literal ID="Literal1"
                            runat="server"
                            Text='<%# Eval("description") %>'>
                        </asp:Literal>
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
        </asp:GridView>

        <p>overflow:hidden で制限 (フォント: メイリオ, 16px)</p>
        <asp:GridView ID="GridView2"
            runat="server"
            AutoGenerateColumns="False">
            <Columns>
                <asp:BoundField DataField="id" HeaderText="id" />
                <asp:TemplateField HeaderText="description">
                    <ItemTemplate>
                        <div class="style1">
                            <asp:Literal ID="Literal1"
                                runat="server"
                                Text='<%# Eval("description") %>'>
                            </asp:Literal>
                        </div>
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
        </asp:GridView>
    </form>
</body>
</html>

Tags: , , , ,

ASP.NET

ASP.NET の CSRF 対策

by WebSurfer 2. May 2023 16:45

Visual Studio のテンプレートを使って作成する ASP.NET MVC と Web Forms プロジェクトに組み込まれている Cross-Site Request Forgery (CSRF) 対策について書きます。

Cross-Site Request Forgery (CSRF)

上の図は、Microsoft のドキュメント「ASP.NET Core でクロスサイト リクエスト フォージェリ (XSRF/CSRF) 攻撃を防止する」に述べられている CSRF 攻撃の仕組みを図示したものです。

どういうことかと言うと、(1) ユーザー認証が必要な正規サイトにログインしたクライアントが、ログアウトせず(即ち、認証クッキーを持ったまま)作業を続ける。(2) その後、悪意のあるサイトにアクセスして攻撃用の HTML フォームが仕組まれたページを閲覧し、その攻撃用の HTML フォームのボタンをクリックする。(3) それにより正規サイトに POST 要求がかかるが、認証クッキーも同時に送信されるので、認証ユーザーが実行を許可されているアクションを実行できる・・・ということです。

その攻撃を防止するための組み込みの機能について以下に書きます。

(1) MVC の場合

Microsoft のドキュメント「Web Stack Runtime XSRF の軽減策」に書いてあるように、2 つの CSRF 対策用トークンを用います。

まず、サーバが入力フォームをクライアントに送信する際、クッキーと隠しフィールドに CSRF 対策用トークンを設定してクライアントに渡します。

クライアントが入力フォーム上で入力を済ませて、サーバーにフォームを POST 送信する際、同時にクッキーと隠しフィールドの CSRF 対策用トークンもサーバーに送信されます。

サーバーは、両方のトークンが比較チェックに合格した場合にのみ要求の続行を許可します。不合格の場合はサーバーエラーとなります。

上の図で、「罠を仕込んだページ」の「攻撃用の HTML フォーム」には CSRF 対策用トークンを持った隠しフィールドは存在しないので比較チェックは不合格となり、CSRF 攻撃を防ぐことができるということになります。

(2) Web Forms の場合

Web Froms には状態管理の手段の一つに ViewState というものがあって、隠しフィールドに状態を保存してクライアントに送信し、ポストバックされたときにサーバー側で隠しフィールドから状態情報を取得するという仕組みになっています。

ViewState には EnableViewStateMac という改ざんを検証する機能があって、デフォルトで有効になっているので、ある程度それで CSRF を防ぐことができます。

ただ、それだけでは不十分だそうで、Page.ViewStateUserKey プロパティを利用して、ViewState に個々のユーザーの 識別子を割り当てることが推奨されています。

ViewStateUserKey プロパティを設定した場合、ASP.NET は、ポストバックによってクライアントから送信されてきた隠しフィールドの ViewState から識別子を抽出し、実行中のページの ViewStateUserKey と比較します。 2 つが一致する場合要求は正当と見なされ、それ以外の場合は例外がスローされる仕組みになっているそうです。(そのあたりの詳しい説明は ViewStateUserKey を見てください)

Visual Studio 2022 のテンプレートで作る Web アプリケーションプロジェクトの場合、マスターページ (Site.Master.cs) に、Page.ViewStateUserKey プロパティを利用して CSRF 対策を強化するためのコードが実装されています。

それがどうなっているかを説明します。

上の Microsoft のドキュメントでは Session.SessionID を Page.ViewStateUserKey プロパティに設定する例が紹介されています。ただ、Session を使わない限り SessionID は発行されないので、そうはできないケースがあります。

なので、Site.Master.cs では、SessionID に代えて Guid の文字列を Page.ViewStateUserKey プロパティに設定しています。そのコードは以下の通りです。

Page_Init で、要求ヘッダに __AntiXsrfToken という名前のクッキーが含まれ、かつ、その値を Guid にパースできる場合、そのクッキーの値を Page.ViewStateUserKey に設定しています。

要求ヘッダに __AntiXsrfToken という名前のクッキーが含まれない場合、含まれていてもその値を Guid にパースできない場合は、Guid を生成してその文字列を Page.ViewStateUserKey に設定します。さらに、__AntiXsrfToken という名前のクッキーを作成し、その値に生成した Guid の文字列を設定してクライアントに送信しています。

using System;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.AspNet.Identity;

namespace WebForms1
{
    public partial class SiteMaster : MasterPage
    {
        private const string AntiXsrfTokenKey = "__AntiXsrfToken";
        private const string AntiXsrfUserNameKey = "__AntiXsrfUserName";
        private string _antiXsrfTokenValue;

        protected void Page_Init(object sender, EventArgs e)
        {
            // 以下のコードは、XSRF 攻撃からの保護に役立ちます
            var requestCookie = Request.Cookies[AntiXsrfTokenKey];
            Guid requestCookieGuidValue;
            if (requestCookie != null && 
                Guid.TryParse(requestCookie.Value, 
                              out requestCookieGuidValue))
            {
                // Cookie の Anti-XSRF トークンを使用します
                _antiXsrfTokenValue = requestCookie.Value;
                Page.ViewStateUserKey = _antiXsrfTokenValue;
            }
            else
            {
                // 新しい Anti-XSRF トークンを生成し、Cookie に保存
                _antiXsrfTokenValue = Guid.NewGuid().ToString("N");
                Page.ViewStateUserKey = _antiXsrfTokenValue;

                var responseCookie = new HttpCookie(AntiXsrfTokenKey)
                {
                    HttpOnly = true,
                    Value = _antiXsrfTokenValue
                };
                if (FormsAuthentication.RequireSSL && 
                    Request.IsSecureConnection)
                {
                    responseCookie.Secure = true;
                }
                Response.Cookies.Set(responseCookie);
            }

            Page.PreLoad += master_Page_PreLoad;
        }

        // ・・・中略・・・

    }
}

さらに、初期画面の要求の処理を行う際 Page_PreLoad で ViewState に Page.ViewStateUserKey の文字列とユーザー名(ユーザーが認証を受けている場合)を設定し、ポストバックの際はそれらが一致しているか否かの検証を行い一致しない場合は例外をスローするという操作を、上のコードで Page.PreLoad にアタッチしたイベントハンドラ master_Page_PreLoad で行っています。

そのコードは以下の通りです。

protected void master_Page_PreLoad(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        // Anti-XSRF トークンを設定します
        ViewState[AntiXsrfTokenKey] = Page.ViewStateUserKey;
        ViewState[AntiXsrfUserNameKey] = 
                    Context.User.Identity.Name ?? String.Empty;
    }
    else
    {
        // Anti-XSRF トークンを検証します
        if ((string)ViewState[AntiXsrfTokenKey] != _antiXsrfTokenValue ||
            (string)ViewState[AntiXsrfUserNameKey] != 
                         (Context.User.Identity.Name ?? String.Empty))
        {
            throw new InvalidOperationException(
                "Anti-XSRF トークンの検証が失敗しました。");
        }
    }
}

ただし、上に書いた「攻撃用の HTML フォーム」で POST 要求するような場合は ViewState が含まれないので ViewState Mac の検証機能は動きません。また、ポストバックと判定されないので master_Page_PreLoad によるトークンの検証もされません。

それで CSRF 対策になるのかというのが疑問でしたが、普通に ASP.NET Web Forms アプリでやるように、ユーザー入力に TextBox コントロール使い、Button コントロールをクリックしてポストバックし、Button の Click イベントで処置を行うケースでは問題なさそうです。

「攻撃用の HTML フォーム」から送信された値は TextBox.Text に代入されることはなく(TextBox.LoadPostData が呼ばれないので)、Click イベントも発生しないのでサーバー側では何も起こりません。

ちょっと問題なのは「攻撃用の HTML フォーム」は POST 要求を出すのでそれを受けた正規 Website は応答を返すという点です。予期せぬ応答をもらったユーザーはびっくりすると思いますが、その対策までは上のコードには実装されていません。

Tags: , , ,

ASP.NET

User-Agent Client Hints

by WebSurfer 21. April 2023 15:27

Windows OS のブラウザから、要求ヘッダに含まれて Web サーバーに送信されてくる従来の User-Agent では、OS のバージョンが Windows 10 なのか Windows 11 なのかの識別ができません。

しかし、Microsoft のドキュメント「User-Agent クライアント ヒントを使用してWindows 11と CPU アーキテクチャを検出する」によると、User-Agent Client Hints を利用すればサーバー側で OS のバージョンが Windows 10 なのか Windows 11 なのかの識別ができるとのことなので検証してみました。

なお、ドキュメントには書いてありませんが、HTTPS 通信に限ると言うところに注意してください。また、現時点では実験的な機能であり、対応ブラウザも Edge, Chrome, Opera に限られている点にも注意してください。(参考 : ユーザーエージェントクライアントヒント API

ブラウザはデフォルトで要求ヘッダに User-Agent Client Hints 関係の情報 sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform を含めます (HTTPS 通信の場合に限ります)。Windows 10 22H2 の Microsoft Edge 112.0.1722.48 の場合以下の通りとなります。

sec-ch-ua: "Chromium";v="112", "Microsoft Edge";v="112", "Not:A-Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

上の情報からは OS が Windows だというのは分かりますがバージョンは分かりません。バージョン情報もブラウザが送信するようにするには、Web サーバーからの応答ヘッダに、

Accept-CH: Sec-CH-UA-Platform-Version

を含めます。そうすると、次の要求からブラウザが要求ヘッダに、

sec-ch-ua-platform-version: "10.0.0"

という OS のバージョン情報を含めて送信してくれるようになります。 (上の例で OS は Windows 10 22H2)

ただし、ブラウザからの最初の要求にはバージョン情報は含まれず、次の要求からになります。そこが使いにくい点かもしれません。

そこが問題であれば、上に紹介した Microsoft のドキュメントの「検出パフォーマンスの最適化 Critical-CH」セクションに書いてあるように、Accept-CH と一緒に Critical-CH ヘッダーを応答ヘッダに含めてクライアント(ブラウザ)に送るのが良さそうです。

具体的には、例えば、Web サーバーからの応答ヘッダに、

Accept-CH: Sec-CH-UA-Platform-Version
Critical-CH: Sec-CH-UA-Platform-Version 

を含めると、ブラウザは最初の応答を受けた後、直ちに再度要求を出し、その要求ヘッダには Critical-CH ヘッダーに指定された情報(この例では sec-ch-ua-platform-version: "10.0.0")を含めて送信してくれます。

以降は、ユーザーがブラウザの「Cookie およびその他のサイト データ」を消去しない限り、ブラウザは指定された情報を送信し続けます。

また、再度 Accept-CH と一緒に Critical-CH ヘッダーを応答ヘッダに含めて送信しても 2 回要求が出るということはありません。

以下に、Fiddler で要求・応答をキャプチャした画像を貼って説明を加えておきます。

(1) 最初の要求

(1) 最初の要求

Default.aspx.cs の Page_Load に以下の C# コードを実装して要求をかけたものです。

Response.AppendHeader("Accept-CH", "Sec-CH-UA-Platform-Version");
Response.AppendHeader("Critical-CH", "Sec-CH-UA-Platform-Version");

画像の青枠部分がブラウザからの要求ヘッダに含まれる User-Agent Client Hints 情報です。この時点では、バージョン情報 Sec-CH-UA-Platform-Version は含まれていません。

画像の赤枠部分に示した通り、Web サーバーからの応答ヘッダには上の C# コードで指定した Accept-CH と Critical-CH が設定されています。

(2) 自動的に再度要求が出る

(2) 自動的に再度要求が出る

ブラウザは Web サーバーからの応答ヘッダの Accept-CH と Critical-CH を見て自動的に再度 Default.aspx に要求を出します。

画像の青枠の通り要求ヘッダにバージョン情報 sec-ch-ua-platform-version が含まれています。

(3) 他のページを要求

(3) 他のページを要求

Web サーバーからの応答ヘッダに Accept-CH, Critical-CH を含まない他のページ(この例では Contact.aspx)を要求してみます。

画像の青枠の通り要求ヘッダに sec-ch-ua-platform-version が含まれています。

(4) 再度 Default.aspx を要求

(4) 再度 Default.aspx を要求

ブラウザから再度 Default.aspx を要求してみます。画像の赤枠の通り Web サーバーからの応答ヘッダには Accept-CH と Critical-CH が含まれていますが、、上の (2) のように再度要求が出ることはありません。


ASP.NET の C# のコードでブラウザからの要求ヘッダに含まれる Sec-CH-UA-Platform および Sec-CH-UA-Platform-Version の情報を取得するには以下のようにします。

string platform = Request.ServerVariables["HTTP_SEC_CH_UA_PLATFORM"];
string version = Request.ServerVariables["HTTP_SEC_CH_UA_PLATFORM_VERSION"];

上のコードで、platform, version に取得された文字列はダブルクォーテーション " で囲まれるので注意してください。

Tags: ,

ASP.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  October 2023  >>
MoTuWeThFrSaSu
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

View posts in large calendar