見出し画像

[Drogon]フィルタの適用

こんにちは、もしくはこんばんは、みじんこきなこです。

このよちよち★Drogonフレームワーク一連の記事で紹介しているDrogonは、Webアプリケーションを作成するフレームワークです。

そして、Webアプリケーションは多くの場合インターネットを通して不特定多数のユーザに公開して、サービスを提供することになるのですが、残念ながらインターネットを利用しているユーザが必ずしも善良なユーザとは限りません。

日々サイバー攻撃というキーワードを含むニュースが世界中で発信されることからも分かる通り、頑張って作ったWebアプリケーションもインターネットに公開すれば何時攻撃されてもおかしくない状態になります。

加えて、先日サポートの終了したInternet Explorerのように、こちらの意図していない表示や動作を行う可能性があるブラウザを使用しているユーザというのも存在します。

今回は、Drogonで作成したアプリケーションに対して、そのような望まないアクセスをブロックする、
フィルタという機能の利用の仕方について解説します。

プロジェクトの作成まで

今回のプロジェクトは以下のような形で作ります。
手順については以前の記事を参照してください。

  • プロジェクト
    名称:filter_sample

  • コントローラ
    種類:filteredController

  • CSPファイル
    名称:passedResponse.csp

こんな感じの名称でプロジェクトとコントローラをdg_ctlコマンドで作成し、viewsの下にCSPファイルを配置していきます。

フィルタの作成

これまでの手順に加えて、今回はフィルタを作成します。
フィルタの作成方法は、まずプロジェクトディレクトリのfiltersディレクトリの下に移動し、以下のコマンドを打ちます。

$ dg_ctl create filter browserFilter.cc
$ dg_ctl create filter IPFilter.cc

今回は、特定のブラウザをブロックするためのブラウザフィルタと、特定のIPからのアクセス以外をブロックするためのIPフィルタを実装してみます。

フィルタクラス

dg_ctlのサブコマンド、create filterで作成されるフィルタクラスは、以下のような形で生成されます。

  • filter.h

/**
 *
 *  filter.h
 *
 */

#pragma once

#include <drogon/HttpFilter.h>
using namespace drogon;


class filter : public HttpFilter<filter>
{
  public:
    filter() {}
    virtual void doFilter(const HttpRequestPtr &req,
                          FilterCallback &&fcb,
                          FilterChainCallback &&fccb) override;
};

  • filter.cc

/**
 *
 *  filter.cc
 *
 */

#include "filter.h"

using namespace drogon;

void filter::doFilter(const HttpRequestPtr &req,
                         FilterCallback &&fcb,
                         FilterChainCallback &&fccb)
{
    //Edit your logic here
    if (1)
    {
        //Passed
        fccb();
        return;
    }
    //Check failed
    auto res = drogon::HttpResponse::newHttpResponse();
    res->setStatusCode(k500InternalServerError);
    fcb(res);
}

開発するときにいじらなければならないのは、まずフィルタを通過させる条件。

if(1)

の条件を変更してやればよいので分かりやすいですね。

そして、ステータスコードです。

res->setStatusCode(k500InternalServerError);

このステータスコードについては、Drogonの中で定数として持っていますので、それを選んで入れるだけです。
やろうと思えばステータスコードを返している部分を自サービスの別のリクエストとしてリダイレクトすることも可能ですが、今回はそれについては解説しません。

動作的には、FilterChainCallbackに登録されている次のフィルタを呼び出すよう、fccb()関数を呼んでDrogonに指示を出すことがフィルタを通過したということになります
登録されたすべてのFilterChainCallbackでfccb()が呼び出されると、初めてコントローラに処理が渡ります。

ブラウザフィルタ

何れのフィルタも、基本的に前回の記事で実験したHttpRequestPtrから取得できる情報でフィルタリングします。
そのことからわかる通り、今回の記事で防ぐことができるのは、アプリケーション層での不正アクセスだけですので、
第6層以下の層での不正アクセスは別の方法で防ぐ必要があることに注意が必要です。

  • browserFilter.h

/**
 *
 *  browserFilter.h
 *
 */

#pragma once

#include <drogon/HttpFilter.h>
using namespace drogon;


class browserFilter : public HttpFilter<browserFilter>
{
  public:
    browserFilter() {}
    virtual void doFilter(const HttpRequestPtr &req,
                          FilterCallback &&fcb,
                          FilterChainCallback &&fccb) override;
};

  • browserFilter.cc

/**
 *
 *  browserFilter.cc
 *
 */

#include "browserFilter.h"

using namespace drogon;

void browserFilter::doFilter(const HttpRequestPtr &req,
                         FilterCallback &&fcb,
                         FilterChainCallback &&fccb)
{
    //Edit your logic here
    if (
        req->getHeader("User-Agent").find("MSIE") == std::string::npos &&
        req->getHeader("User-Agent").find("rv:") == std::string::npos &&
        req->getHeader("User-Agent").find("Trident") == std::string::npos &&
        req->getHeader("User-Agent").find("Edg") == std::string::npos
    )
    {
        //Passed
        fccb();
        return;
    }
    //Check failed
    auto res = drogon::HttpResponse::newHttpResponse();
    res->setStatusCode(k500InternalServerError);
    fcb(res);
}

ブラウザフィルタは、アクセスがあったhttpリクエストのヘッダから、User-Agentというキーの値を取り、その特定の値に対してブロックしています。
MSIE(Internet Explorer10.0以下)とrv:(Internet Explorer11)の二種類と、Tridentをブロックしています。

このTrident、Internet Explorerで長らく使用されていたレンダリングエンジンですが、ブラウザがサポート終了しましたし開発も終了しています。
ただ、未だにVBA等でブラウザコンポーネントを使用したり、WindowsMediaPlayerなどのブラウザを内部に組み込んだアプリケーションでは使用している場合がありますのでブロックしています。

また、私の開発環境ではInternet Explorerをアンインストールしてしまっているので、実験用にMicrosoft Edgeもブロックしています。

IPフィルタ

IPでブロックするフィルタには、Drogon自体に組み込まれている、

  • drogon::IntranetIpFilter
    ローカルマシンからのアクセスを禁止する。

  • drogon::LocalHostFilter
    ローカルマシン以外からのアクセスを禁止する。

というフィルタも存在しますが、今回はIP個別にブロックする実験ですので、カスタムフィルタを用意しています。

  • IPFilter.h

/**
 *
 *  IPFilter.h
 *
 */

#pragma once

#include <drogon/HttpFilter.h>
using namespace drogon;


class IPFilter : public HttpFilter<IPFilter>
{
  private:
    std::map<std::string, bool> filteringIPMap;
    void initializefilteringIPMap() noexcept;

  public:
    IPFilter(){
      initializefilteringIPMap();
    }
    virtual void doFilter(const HttpRequestPtr &req,
                          FilterCallback &&fcb,
                          FilterChainCallback &&fccb) override;
};

  • IPFilter.cc

/**
 *
 *  IPFilter.cc
 *
 */

#include "IPFilter.h"

using namespace drogon;

void IPFilter::doFilter(const HttpRequestPtr &req,
                         FilterCallback &&fcb,
                         FilterChainCallback &&fccb)
{
    auto filterResult = false;
    if( filteringIPMap.find(req->getPeerAddr().toIp()) != filteringIPMap.end() )
    {
        filterResult = filteringIPMap[req->getPeerAddr().toIp()];
    }

    //Edit your logic here
    if (filterResult)
    {
        //Passed
        fccb();
        return;
    }
    //Check failed
    auto res = drogon::HttpResponse::newHttpResponse();
    res->setStatusCode(k500InternalServerError);
    fcb(res);
}

void IPFilter::initializefilteringIPMap() noexcept
{
    filteringIPMap.insert(std::pair("192.168.0.2", false));
    filteringIPMap.insert(std::pair("192.168.0.3", false));
    filteringIPMap.insert(std::pair("192.168.0.4", false));
    filteringIPMap.insert(std::pair("192.168.0.5", false));
    filteringIPMap.insert(std::pair("192.168.0.6", false));
    filteringIPMap.insert(std::pair("192.168.0.7", false));
    filteringIPMap.insert(std::pair("192.168.0.8", false));
    filteringIPMap.insert(std::pair("自分のIP入れてね", true));
}

今回はreqポインタからIPをtoIp()で文字列として取っていますので、std::mapで用意し、それぞれに通すか禁止するか選択できる構造にしています。

IPの範囲でフィルタリングする場合もあると思いますが、その場合はtoIp() を ipNetEndian()またはip6NetEndian()に差し替えて、帰ってくる値を範囲で禁止または通過するようにしてやります。

例えばよくある 192.168.0.0 ~192.168.0.255 までを許可したいなら以下のようになります。

if( c0a80000 <= req->getPeerAddr().ipNetEndian() &&
req->getPeerAddr().ipNetEndian() <= c0a800ff )

コントローラへのフィルタ適用

フィルタを作成したら、今度はコントローラ側に適用するフィルタを設定します。

フィルタの設定は非常に簡単で、従来のMETHOD_ADDの第四引数移行に作成したフィルタのクラス名を設定するだけです。
この際、フィルタを名前空間を区切って作成した場合はその名前空間についても記載する必要があることに注意が必要です。

今回作成したフィルタをサンプルの関数に登録しています。

METHOD_ADD(filteredController::filteredRequest, "/", Get, "browserFilter", "IPFilter"); 

このフィルタそのものはいくつでも登録できますし、異なるURLに対して同じフィルタをそれぞれ適用することもできます。
但しフィルタの動作も独立したスレッドで行われる仕組みですので、同一のフィルタを複数ページに設定する場合はフィルタメソッドがリエントラント(再入可能)な仕組みになっていることが必要です。

そしてフィルタリングされて呼び出されるメソッドはcspを呼び出しているだけです。

CSPファイル

これはさして重要でないので、今回は以下のような物を用意しています。

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta http-equiv="Content-Style-Type" content="text/css">
    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">

    <meta name="description" content="DrogonのCSS利用サンプル">
    <meta name="keywords" content="Drogon,CSS,MVCフレームワーク,C++,高速">
    <meta name="Mizinko-Kinako" content="MKnote">
    <meta name="copyright" content="Mizinko-Kinako">
    <meta charset="UTF-8">
    <title>Filter通過</title>
    
</head>

<body>
    <p>やぁ!ようこそ! 無事通過したよ!</p>
</body>
</html>
 

ビルド

ビルドも普段通りbuildディレクトリの下で、

cmake .. && make

を実行するだけです。

実行

出来上がったバイナリを実行してみましょう。

$ ./filter_sample

そしてブラウザからアクセスしてみます。

Chromeでアクセスした場合は以下のような形で通過したメッセージが表示されます。

通過してページが表示される

Edgeでアクセスすると、500のエラーが飛んできます。
私の環境ではVirtualBox上のサーバにポートフォワーディングで飛ばしているため、ポートが50051ですが、実際は80などでアクセスします。

500エラーと共にブロックされる

さいごに

今回はフィルタを使用した、Drogonアプリへのアクセス制御について書きました。
インターネットで動作するシステムは、常に不正アクセスの危険にさらされていると言っても過言ではありません。

せっかく一生懸命作ったサービス、システムでも、いざリリースした途端そうした不正アクセスによって破壊されてしまっては、努力が無駄になってしまいます。

そうした事態を防ぐためにも、今回紹介したような内容をうまく利用して、しっかりとセキュリティを確保していただければと思います。

いいなと思ったら応援しよう!