外部サイトへの遷移させるとき注意すべき1つのこと

SNSなどプライベートな記事をユーザが書けるページから、外部サイトへリンクする場合、リファラーによって秘密のURLが漏洩しないように、リダイレクト専用のアプリを用意することがあります。
また、自サイトからの外部サイトへの遷移を把握するためにも、そういうリダイレクト専用アプリを用意してログを落とすようにすることがあります。

これは、特に何もしないと格好のフィッシング詐欺の踏み台になります。

https://www.facebook.com/l.php?u=http://i.am.fishing/

みたいなリンクを、どこかの掲示板などに貼られると、「おっ、フェイスブックのリンクじゃん」
ってことで、踏んでしまう人がいるからです。

対策です。

## :skull: リファラチェック

HTTP_REFERERが自サイト以外だったら、エラーにする設計です。あまりないとは思いますが、こういう設計をされているサイトを見かけたことがあります。

当然ながらRefererはクライアントの設定で送らないように容易にできます。(auのガラケーブラウザはRefererを送らないってことも、昔はありました。)

そういう、ふつうのユーザが、外部サイトへの遷移ができなくなってしまうので、この設計はやめましょう。

## :confused: ホワイトリスト

ある程度外部リンクをコントロールできる場合は、遷移可能なサイトへのURLをリスト化しておき、チェックする方式をとることがあります。

たいていリダイレクト専用のアプリケーションなんて汎用的なものは、時間が経つと、当初と違う用途で使われたりするようになります。そうすると、最初は大したことないから、と始めたホワイトリストのメンテナンスがエラい大変になってきた、なんてことになります。(みたいなことが以前ありました)

なので、そういう恐れがあれば、これもやめたほうが良いです。

## :grinning: ハッシュコード

これがふつうの方式です。ロケーションURLを元にしたHMACを付けます。これがない、またはHMAC値が一致しない場合は、エラーにするか、"外部サイトに移動しようとしてますがよろしいですか?"みたいなページを挟むようにします。

![image](https://qiita-image-store.s3.amazonaws.com/0/8475/17063db1-4ae5-279a-5d0f-4662fa7b97a9.png)

Facebookだとこんな感じです。

### Javaでの実装

HMACの鍵でURLを作ります。

```java
    public String redirectUrl(String url) throws UnsupportedEncodingException {
        return "/redirect?u=" +  URLEncoder.encode(url, "UTF-8")
                + "&h="+ HmacUtils.hmacSha256Hex(hmacKey, url);
    }
```

そうすると、リダイレクト用のURLを作ることができます。

```
/redirect?u=http%3A%2F%2Fwww.yahoo.co.jp%2F&h=55fba8d6d44d8f2bd553cd76e018a06318860d5b748a4995292602ad615422de
```

リダイレクト用のアプリケーションは、それを突き合わせる以下のような感じになります。

```java
public class RedirectServlet extends HttpServlet{
    private String key;
    private String confirmPath;

    public void init() throws ServletException {
        ServletConfig config = getServletConfig();
        key = config.getInitParameter("key");
        if (key == null) {
            key = UUID.randomUUID().toString();
        }
        confirmPath = config.getInitParameter("confirmPath");
    }

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        URL url;

        String userHash = request.getParameter("h");
        try {
            String u = request.getParameter("u");
            url = new URL(u);
        } catch (Exception e) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        try {
            String digestHex = HmacUtils.hmacSha256Hex(key, url.toString());
            if (userHash != null && userHash.equals(digestHex)) {
                response.sendRedirect(url.toString());
                return;
            } else if (confirmPath != null) {
                RequestDispatcher dispatcher = request.getRequestDispatcher(confirmPath);
                dispatcher.forward(request, response);
            } else {
                response.sendError(HttpServletResponse.SC_FORBIDDEN);
            }
        } catch (Exception e) {
            log("something wrong", e);
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }

}
```

これで、Facebookの`l.php`と同じような機能を作ることができます。

## まとめ

HMACによる改ざん検知は、Javaでは簡単に使えて、いろいろ役に立ちます[^1]。

[^1]: [静的コンテンツのトークンベースのアクセス制御](http://qiita.com/kawasima/items/f9ecf20e8d8ac7f126e9) とか。