見出し画像

Webエンジニアのセキュリティ特集

はじめに

みなさんこんにちは、ALH開発事業部のREIYAです。

今回はWebエンジニアがアプリレイヤで意識するべきセキュリティ観点をまとめました。
インフラレベルでの対応も知っておくべきですが、とりあえずアプリ側でできる制御を具体的にまとめます!


結局こういうのは手を動かしたほうが実感できるので、Try Hack Meなどをはじめとするこれらのサイトで実際にハッキングしてみると楽しいです。

ハッシュ化とストレッチング

昨今CognitoなどがありますのでDB腹持ちすることがなくなりつつありますが、パスワード等をまさか平文のまま登録するわけにはいきませんので
ハッシュ化をすることで復元が現実的でない状態にします。
難しい話抜きでただ使うためだけの説明をします。

平文 → ハッシュ文字列 にすることをハッシュ化。

passw0rd -> aU7sHJs6lsPjaFAEdhk....

また、ハッシュ化した場合、元の平文に戻すことは現代の計算機科学的に困難です。
余談ですが、ハッシュ化でなく「暗号化」は、元に戻せるものです。

また、ハッシュ化にはソルトという文字列が必要ですので
先ほどの例をより正しく書き直すと

passw0rd + ソルト文字 -> aU7sHJs6lsPjaFAEdhk....

となります。平文に戻せない代わりに、同じ平文と同じソルト文字でハッシュ化すると必ず同じ結果の文字列になりますので、毎回ハッシュ化して値の一致を確認します。
ソルトは通常、ハッシュ後の文字列に含めて持っておきます。ソルトだけバレても復元できないのでOK

具体的にはこうです。
Pythonのhashlib.sha256というものは、ハッシュの中でもSHA256というアルゴリズムを用いたハッシュ関数を使用しています。
他にもファイルのチェックサムなどでよく使われるMD5等のアルゴリズムがあります。

import hashlib
def _hash_password(password, salt):
    # hash with sha256
    pass_byte = bytes(password, 'utf-8')
    digest = hashlib.sha256(salt + pass_byte).hexdigest()
    # return salt+hash
    salt_str = salt.decode('utf-8')
    return f'{salt_str}$${digest}' # ソルトがわからなくならないよう、ハッシュ後文字に入れておく

ソルト文字は適当に

import secrets
token = secrets.token_hex()

をbyteにして使ったり

import base64
salt = base64.b64encode(os.urandom(32))

のように生成しておけば良いと思います。個人的には後者がおすすめ。

また、平文とハッシュ値を比較するにはこのように、再度ハッシュ化すればよいですね。

def _hash_check(plain, password):
    salt = password.split('$$')[0]
    digest = _hash_password(plain, bytes(salt, 'utf-8'))
    return password == digest

ストレッチングという、より高セキュアにする手法もあります。
パスワード破りの代表であるbrute force、辞書攻撃等の網羅的な力技だと
例えばハッシュ値とソルトが漏洩した場合、めっちゃいろんな文字でハッシュ化ためしまくれば当たっちゃうことがあります。

  • ハッシュ値(DBが漏れたとする)

  • ソルト(通常ハッシュ値にくっつけて保管される)

  • ハッシュに用いているアルゴリズム(よくSHA256が使われる)

これだけの情報が揃っていると、passw0rd程度の文字ならすぐ破られてしましますので
なんどもハッシュ化する処理にしておく、すなわち

passw0rd + ソルト文字 -> aU7sHJs6lsPjaFAEdhk....

aU7sHJs6lsPjaFAEdhk.... -> もっかいハッシュ化  Ejds7AShh4lxc8t3Asd....

Ejds7AShh4lxc8t3Asd.... -> もっかいハッシュ化  ....
....

のようにすることで、何回ハッシュ化するかという選択肢も設けることで、攻撃者の総当たりに必要な幅を増やすことができます。

以下では先ほどの関数を、1万回ハッシュ化を繰り返した文字を返却するようにしたもの。

import hashlib
def _hash_password(password, salt):
    pass_byte = bytes(password, 'utf-8')
    digest = hashlib.sha256(salt + pass_byte).hexdigest()
    # stretching
    for _ in range(1e4):
        digest = hashlib.sha256(bytes(digest, 'utf-8')).hexdigest()
    salt_str = salt.decode('utf-8')
    return f'{salt_str}$${digest}'

まぁこういったことはなるべく自力実装しないのが一番ですので、ライブラリやサービスがあるなら積極的にそちらを採用しましょう。

cookieフラグ

昨今のWebアプリではフロント側に認証結果を持ちsession管理をすることも多いと思いますが
フロントはJavaScriptで(ほぼ)なんでもできちゃうので、よく気をつけたいですね。
cookieに認証トークンであったりを格納した際、これをバックエンド側へ送信し
HTTPリクエストヘッダ等と併せるなどして認証を行う場合を想定しましょう。

cookieにはsame-siteフラグというフィールドがあります。
ざっくりまとめると、これがあれば別サイトへcookieを送らないようにできるもので、詳しくは長いのでこちらなどを参考にしてください。

よくわからなければとりあえずStrictにしておけばいいです。

各ブラウザでのsame-siteフラグ対応状況はこちらです。
未対応の場合は後述の対策が必須です。

その他のCSRF対策

前述のsame-siteが手軽なCSRF対策のうちの一つですが、ブラウザ未対応であった頃やどうしてもcookie関連では防げない場合でのCSRF対策はどうしたらよいでしょうか。
CSRFについて基本的な部分はこちらが非常に読みやすいのでぜひ。

さて、対策でよく使われるのが

  • CSRFトークン

  • HTTPリファラヘッダ

  • HTTPカスタムヘッダ

などです。


CSRFトークン

例えばHTML内部にトークン文字列を埋め込み、毎リクエスト時にリクエストに含める等することで
localStorageやcookieを介さずトークンをストアできるという、いにしえの手法です。
元々こういった手法を、HTTPヘッダにいれろや!と進化していきました。

HTTPリファラ

HTTPヘッダにはRefererというフィールドがあります。JMeter等で負荷試験などやる際によく出てくると思いますが、リクエスト先に行く直前のウェブページのアドレスを持ちます。
直前のアドレスが別サイトであれば攻撃と判断するような処理を入れるなどする方針ですね。

HTTPカスタムヘッダ

たとえば

curl -H 'Content-Type:application/json' https://google.com/

とする時のContent-Typeは予約されたヘッダですが、独自で追加することももちろんできます。
以下のように使われることが多いです

curl -H 'X-Auth-Header: xxxxxxx' https://google.com/aaa/...

認証関連のヘッダはAuthorizationが証明書用に予約されています。
Xから始まるものはユーザカスタム系のヘッダと昔はなってましたが、最近ではいろいろ物議を醸しています。

参考


で、結局これらをどう使うかは自由ですが、独自実装よりもやはりライブラリに頼りたいということで
各言語でよく使われるサーバのライブラリでのCSRF対策まとめがこちらにありました。

ディレクトリトラバーサル

たとえば

https://test.com/../../file.txt

みたいなことをされて大丈夫かということです。
apacheの初期設定等ではサーバ中の

/var/www/html/

このパス配下について公開に設定されていますが
ディレクトリへアクセスした際、ブラウザ上なのにエクスプローラのようにファイル一覧が見られてしまうことがありますよね?

以下のように、サーバのファイルシステムへのアクセスを許可しない設定を入れることで基本的には対応できますが

<Directory />                   
  AllowOverride none↲
  Require all denied↲
</Directory>

サーバ内でlocalhostを立てた際などに、プロジェクトフォルダ配下に鍵ファイルを残していたりした場合、予期せぬ公開に備えなくてはいけないので

鍵ファイルは必ず別の場所で管理するようにしましょう。

XSS

セッション系の攻撃がCSRFですが、XSSは入力欄に任意コードいれでドーンというものですね。
対策はひとえにサニタイズするだけですが、任意コードを実行できる脆弱性はいまだに潜んでいることが多いです。
昨今の身近な例ではlog4jxz-utilのバックドアなどがありました。

apacheのlog4jはJava系のSlf4jなどで気づいたら使っていたなんてことも多いです。xz-utilはmacでaws-cliを入れていればbrewの依存関係で必ず入っていますし、身近すぎて怖いですね。

ところでdataURLというものをご存知でしょうか?
以前私の書いたこちらのブラウザ簡易エディタであったり、これなんかもdataURLで表示することだってできます。

data:text/html,<canvas id=C><script>f=()=>{R=n*3**.5/4,p=p.flatMap(([x,y])=>{t=[[x-n/4,y-R],[x+n/4,y+R],[x-n*3/4,y+R]];return ~k?k?t:[t[0]]:p}),n/=2;p.map(([x,y])=%3E{with(C.getContext`2d`){beginPath();moveTo(x,y);lineTo(x-n,y);lineTo(x-n/2,y+(~k?R:-R));closePath();stroke()}});k++%3CM&&setTimeout(f,a)};f(M=6,k=-1,p=[[C.width=C.height=a=1e3,a]],n=a*2)%3C/script%3E

ブックマークレットだって結局はURL上でJSを実行できる仕組みです。

リテラシーの低い方向けに、devツールへのコピペ禁止対策が行われている某狐ブラウザもあります。サイトによっても注意書きをconsoleに出しているものもあります。

とにかく任意のJSをブラウザでどうにでも実行できてしまうので、XSSは厄介ということです。実際フロントエンドをJSで動かしている以上全ての任意なスクリプト実行を無効にすることは絶対に不可能で

  • devツールのネットワークタブからリクエストのfetchコードを取得
    これによってJSのfetchでのリクエストをconsoleから行える

  • applicationタブからsessionStorage、localStorage、indexedDB等を参照できる

  • Sourceタブからwebpack後であればjsが見れる

  • React等であっても拡張機能を用いてDOMツリーdebugができる

などなど、開発者にとって便利な機能は全てハッカーにも便利です。

余談ですがWebアプリの全てをURLに入れてしまおうという記事もあります。

このように、入力フォーム以外からの攻撃をXSSと呼ぶかは定かではありませんが、広義的に任意コード実行と捉えると
少なくとも我々開発者側が取れる防御策は

  • サニタイズ

  • API側での徹底したバリデーション

  • 不要なフロントStorageへの格納をなくす

  • そもそも不要な情報をフロントに渡さない

などを徹底することしかありません。

SQLインジェクション

XSSの中でもSQLに特化したものがこちらですね。
サニタイズしたら終わりなんですが、preparedStatementなど
ORMapperライブラリ等を用いないベタな言語↔︎SQLやりとりでは、よくサニタイズが忘れられがちな気がします。

よくあるのが

SELECT * FROM table WHERE 1 = 1 OR ... ;

のように、1 = 1 ORという、後ろに何をつけても絶対に真になる条件を送るやり方ですね。

情報の抜き取りとしてはこれ等が挙げられますが、最も恐ろしいのはトランザクションの効かない破壊的操作TRUNCATEDROPです。
(トランザクションが効いたところでコミットされてたら意味ないけども)

前述のXSSと同様な対策しかありませんし、昨今のライブラリはどれもしっかりしてますので
下手に独自実装でもしない限り踏むことはないと思います。

桁溢れ

さて、サニタイズやバリデーションだけでは地味に防げないものがあります。
攻撃とはちょっと違いますが、意図的に起こせてしまうバグの対策が必要で
プログラム上で表現できる桁数の限界を突破する場合です。

前述のようにfetchAPI等でJSからいくらでも好きな値をバックエンドに送れるわけですが、JSONでAPIに値を渡す際以下のJSON.key.valは数値として解釈されますが

{
    "key": {
        "val": 1000000000000000000000000000000
    }
}

これは$${10^{30}}$$です。
さて、これをバックエンド側で受け取ったあとにJSONをオブジェクトへ変換すると
大体の場合$${-2147483647}$$になっていることでしょう。
intは通常32bit整数、longは64bitであることが多いです。doubleは64bit浮動小数ですが、C言語などでは桁数表現に11bit程度を使い
52bit程度しか精度がありません。

ちなみにPythonでは桁とかないので溢れることはありませんが重いです。
だいたいですが、intは$${10^9}$$程度まで、longなどであれば18乗程度までと覚えておきましょう。

CORS

こちらは対策できるものというより、CORSポリシー違反でAPI疎通ができない!みたいなことが多いので載せておきます。
よくCORSエラーでつまずく人が多い印象ですが、確かにいろいろなことを知る必要があってややこしいです。
いろんな記事がありますが、とにかくわかりにくいのですごく簡単にまとめてみました。


  1. 今、あなたはバックエンドサーバをhttps://test.comとして建てました

  2. さらにAPIをhttps://test.com/api1
    にリクエストするように作りました。

  3. フロントのJSからajax(axiosでもfetchでもXmlHttpRequestでもよい)でここにアクセスして、JSONを受け取りたいとします。

  4. フロントはReactなどで作ったのでlocalhost:3000だとする

よくあるやつですね。


衝撃の事実

  • Cross Origin つまり違うドメイン間だと、JSからバックエンドへAPIリクエストできない!

  • 設定するとできるようになる!

Origin = ドメインと思っておいて良いです。
ポートも含めFQDNが同じものがSameOriginです。参考

よって、localhost:3000からhttps://test.com/api1へのリクエストは、CrossなOriginへのリクエストですね?

だからダメです。そういうもんなんです。諦めてください。

正しい挙動

フロントは変えなくて良いです。バックエンド側で、アクセスを許すドメインを登録するだけです。

1️⃣フロント→バックへリクエスト
2️⃣バック→フロントへレスポンス

としていましたね?今度からは

1️⃣フロント→バックへプリフライトリクエスト
2️⃣バック→フロントへいったんレスポンス
3️⃣フロント→バックへリクエスト
4️⃣バック→フロントへレスポンス

となるだけです。事前に1回やりとりするだけ。

より詳しい実装はものによりますがたとえばGinならこんな感じです。

package main

import (
	"io/ioutil"
	"log"
	"net/http"
	"time"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	// create CORS config
	r.Use(cors.New(cors.Config{
		AllowOrigins: []string{
            // 許可するフロント側のオリジンを設定する
			"http://localhost:3000",
			"http://127.0.0.1:3000",
		},
		AllowMethods: []string{
			"POST",
			"GET",
			"OPTIONS", // for preflight request
		},
		AllowHeaders: []string{
			"Access-Control-Allow-Credentials",
			"Access-Control-Allow-Headers",
			"Content-Type",
			"Content-Length",
			"Accept-Encoding",
			"Authorization",
		},
		AllowCredentials: true,           // need cookie
		MaxAge:           24 * time.Hour, // preflight request's result chache term
	}))

	// regist API endpoints
	rg := r.Group("/api1")

	// エンドポイント追加してく

	r.Run(":8080")
}

欲張りな方向けにflaskも書いときます。
基本的にこういう許可リストは、別ファイルにまとめることが多い印象

from pathlib import Path
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)

# add whitelist for CORS allow
# whilelist.txtに、許可するフロントのドメイン書いてあるものとする
with open(Path(__file__).resolve().parent.joinpath('../whitelist.txt'), 'r') as f:
    CORS(app, origins=f.read().split('\n'), methods=['OPTIONS', 'GET', 'POST'])

# エンドポイントを登録とか

下手書きイメージ

app = Flask(__name__)
CORS(app,
     origins=['http://localhost:8182', 'yourserver domain'],
     methods=['OPTIONS', 'GET', 'POST'])

CPS

サーバ側が許可してないとCORSで弾かれますが
Webページ側がそもそも許可ドメイン以外へのアクセスを禁止することもできます。
JavaScriptには凶悪な、なんでもできてしまう関数 eval がありますので

攻撃者側のサーバがCORS許可してさえいれば
あらかじめ用意したスクリプトをサーバに用意し、Webページ上からjsでサーバへアクセスしeval
とできてしまいますので
Webページ上での任意js実行可能な状態を少しでも改善するすべとして、ContentSecurityPolicyを設定できます。

ヘッダで設定します。以下を参考

(補足) DDoS

やめましょう。

あんまり良くないですが、田代砲をご存知ですか?
某掲示板サイト発祥の、DoS攻撃用スクリプトです。
DoS程度なら簡単に再現できてしまうどころか、気付かぬうちにやっちゃうこともあります。

1つのPCからどこかのサーバへ、めっちゃ大量にリクエストを送って負荷をかけることをDoS攻撃と言いますが
1PC程度じゃ大した負荷になりませんので、複数PCを使って攻撃するバージョンのことをDDoSと言います。

DoS程度なら意図せずやってしまうことがあります。そう、JMeterならね。
負荷試験以外でも、組織契約のRedmineやGitアカウントに対してブラウザからページを大量に開きまくったり、送信ボタン連打しまくると割と落ちます。

落ちるというか、一時的に403 Forbiddenでアクセスを一定時間ブロックされることが多いです。

リクエスト数でクラウドを従量課金している場合等は本当に気をつけてください。

(補足) DMZ

そういったリクエスト、社内網を別ネットワークにしたらよくね?
ということで
外部からのリクエストはDMZで守り、内から外へはプロキシを介してブラックリストフィルタリングすることが大事。

DMZとは、外部から来たリクエストをスイッチングハブを介して内部に到達させない等をし、武装解除をさせるみたいな感じです。

結局こういったインフラ側での対策の上にAppGatewayやWAF、ネットワークセキュリティグループ等を儲け、何重にもブロックするのが通例です。

(補足) traceroute nslookup ssh root

アプリレイヤか定かでないですが、このサーバへのアクセスは

  • どういう経路?

  • 行き先IPは?

  • どうやったら入れる?

等をコマンドで知ります。ざっくりまとめます

traceroute

windowsではtracertです。
経路を知れます。

nslookup

ドメインからIPを知ります。

ssh root

sshコマンドでサーバアクセス。
基本的にサーバは443と22を開けてると思います。
sshアクセス時にrootユーザのパスワードが雑魚だとまずいですね?
また、そもそもrootはsshでアクセスできないようにするのが通例です。
権限が最強なのにユーザIDがバレているからですね。

通例では /etc/ssh/sshd_config に対して

PermitRootLogin yes

を no にするような設定で塞げるはずです

まとめ

Webアプリ開発上

  • パスワードはハッシュ化+ストレッチングしよう、なるべくライブラリ使う

  • CSRF対策にcookieのsame-siteフラグStrict

  • ディレクトリトラバーサル対策と鍵ファイルは別場所で管理

  • XSSやSQL介入対策にサニタイズ

  • 桁溢れ対策も加味したバリデーション

を注意し、フロント↔バック間はCORSポリシーがあるので

  • バックエンド側でPreflightリクエスト許可

  • フロントのドメインを許可しておく

よりXSS等に強くするために

  • CSPを設定し許可オリジン以外へのfetchやevalをブロック

また

  • サーバ側をいたわって、DDoSしない

  • インフラチームに感謝する

  • DMZ WAF等 低レイヤ含め何重にもブロックを設ける

  • インフラチームに感謝する

  • rootアクセス禁止

  • パスワードはちゃんと作る

  • Try Hack Meで遊んでみる

  • インフラチームに感謝する

  • インフラチームに感謝する

という感じです。

ALHについて知る



↓ ↓ ↓ 採用サイトはこちら ↓ ↓ ↓


↓ ↓ ↓ コーポレートサイトはこちら ↓ ↓ ↓


↓ ↓ ↓ もっとALHについて知りたい? ↓ ↓ ↓