4日目: Web アプリケーションのセキュリティ・Web API と GraphQL
おしながき
Web アプリケーションのセキュリティ
Web API と GraphQL
Web アプリケーションのセキュリティ
Web アプリケーションが抱えるリスク
セキュリティが守られていないソフトウェアには大きなリスクがある
ユーザの大切なデータが盗まれたり壊されたり利用不可能に
企業の機密情報の漏洩
Web サイトの脆弱性も格好の標的
セキュリティの定義
セキュリティの三大要素:
機密性: 許可された人物だけがアクセスできる
完全性: 正しい情報を入手でき, 意図された処理を正しく完了できる
可用性: システムが必要なときに利用できる
開発者はこれらを脅かす攻撃からシステムを守る必要がある
代表的な脆弱性と対策
ここでは代表的なものを紹介します
SQL インジェクション
XSS
CSRF
その他いくつか
脆弱性対策
脆弱性対策は大別すると二種類ある
根本的対策: 脆弱性を作り込まない実装をする
保険的対策: 攻撃による影響を軽減する
根本的対策を基本に, もしものときに備えて保険的対策を行う
SQL インジェクション
攻撃者によって意図しない SQL クエリを発行されてしまう
具体例
ユーザーを名前で検索する SQL クエリを以下のように組み立てている
"SELECT * FROM user WHERE name = '" + name + "' LIMIT 1;"
攻撃者がユーザー名に '; DELETE FROM user; -- を入力
ユーザー情報をすべて削除するクエリが発行されてしまう!
SELECT * FROM user WHERE name = ''; DELETE FROM user; --' LIMIT 1;
SQL インジェクションでできること
情報の閲覧・改ざん (DB 操作なんでも)
(実装によっては) 認証の回避
(DB の設定によっては) プログラムの実行・ファイルの参照や更新
SQL インジェクション対策
原因はユーザーの入力をそのまま SQL クエリに含めてしまったこと
"SELECT * FROM user WHERE name = '" + name + "' LIMIT 1;"
対策としては
ユーザーの入力に対して, 適切なエスケープやバリデーション処理を行う
推奨: プレースホルダを使う
プレースホルダ
クエリをプレースホルダ ? を含む形で書き, それを埋める入力を後から与える
code:go
err := r.db.Get(
&user,
SELECT id,name FROM user WHERE name = ? LIMIT 1,
name,
)
プレースホルダ
プレースホルダを使うと, 攻撃者によって意図しない形のクエリを構築されることがなくなる
保険的対策として, 合わせてバリデーションを行っておくのも良い
プレースホルダ
プレースホルダの実装は動的・静的の二種類がある
動的プレースホルダ: ライブラリが正しくエスケープされたクエリを構築する
静的プレースホルダ: DB エンジンが処理する
a.k.a. prepared statement
動的プレースホルダはライブラリの実装に不具合があれば脆弱となるため, 静的プレースホルダを用いるのがより安全
XSS (Cross-Site Scripting)
Web サイト上で意図しないスクリプトを実行されてしまう
具体例
攻撃者が掲示板に以下のような内容を書き込む
<script>while(1)alert("こんにちは")</script>
掲示板は書き込まれた内容をそのまま HTML として出力
ユーザーが掲示板を開くと無限にアラートが表示され続ける
XSS でできること
スクリプトでできることなんでも
無限アラートのような単なる迷惑行為
Cookie に保存されている秘密情報やユーザーの入力を外部へ送信
Web サイト上の表示の改ざん・操作
仮想通貨採掘
etc.
XSS 対策
原因はユーザーの書き込んだ内容を, そのまま HTML として出力してしまったこと
対策としては
ユーザーの入力に対して, 適切エスケープやバリデーション処理を行う
Cookie 側の対策
ブラウザの XSS 防御機能を有効化する
許可しないスクリプトを実行できないように設定する
エスケープ・バリデーション
HTML タグとして解釈されないようにエスケープする
< → <, > → > など
エスケープ・バリデーション
属性値にも注意
例えばリンク先がユーザー入力の場合
<a href="?">Hello</a>
以下のような内容をそのまま出力すると XSS となる
javascript:alert(1)
"><script>alert(1)</script>
" を含めた文字のエスケープや, プロトコルのバリデーションを行う
エスケープ・バリデーション
複雑なエスケープ・バリデーション処理は自分で実装しない
考慮漏れが起こりやすい
信頼できるライブラリに任せるのが吉
課題で使っているテンプレートエンジンは, エスケープ処理やバリデーションを行ってくれるはず
Cookie 側の対策
Cookie に保存されているセッション ID を盗むことができれば, そのユーザーになりすますことが出来てしまう
秘密情報を含む Cookie に HttpOnly 属性を付与すると JavaScript からアクセスできなくなる
これによって万が一 XSS が起きたときのリスクを減らすことができる (保険的対策)
ブラウザの XSS 防御機能
ブラウザには XSS 防御機能が組み込まれていることがあり, 一部の XSS 攻撃 (反射型 XSS) を無力化できる
レスポンスヘッダに X-XSS-Protection: 1; mode=block を付与することで機能を強制的に有効化できる
これも保険的対策として行うと良い
Content Security Policy (CSP)
ブラウザのコンテンツの読み込みやスクリプトの実行を制限する機構
Content-Security-Policy レスポンスヘッダを使うことで利用できる
スクリプトの実行を制限することで XSS に対する保険的対策となる
Content Security Policy (CSP)
例: Content-Security-Policy: default-src 'self'; img-src *; media-src media1.com
基本は Web サイト自身のオリジンからのみファイル・スクリプトを読み込み・実行する
画像については任意のオリジンから, 動画などは media1.com からの読み込みも許可する
<script> タグ内に直接書かれたスクリプト (インラインスクリプト) も実行されなくなる
安全性は増すが開発はちょっと大変になる
話題: ファイルダウンロード時の XSS
HTML 以外のファイル (のつもり) でも XSS は発生し得る
レスポンスの Content-Type ヘッダが正しくないと, ブラウザが HTML として解釈し, スクリプトが実行されることがある
IE では拡張子のみからファイル形式を HTML と推測してしまうことも
悪用されるとファイルアップローダーなどに罠を仕掛けられてしまう
話題: ファイルダウンロード時の XSS
対策としては
まずは Content-Type を正しく設定する
レスポンスヘッダに X-Content-Type-Options: nosniff を設定する
万が一 Content-Type が間違ったりしていても, ブラウザが HTML として解釈することはなくなる
話題: Self-XSS
攻撃者「このスクリプトをコンソールに貼り付けると便利機能が使えるようになるよ!」
実際は悪意のあるスクリプト
対策としては
Cookie の HttpOnly 属性のような, 通常の XSS の保険的対策も有効
CSRF (Cross-Site Request Forgery)
攻撃者が偽造したリクエストをユーザーの権限で送信されてしまう
具体例 1
投稿が GET メソッドで実装されている (!)
https://example.com/post?message=...
攻撃者は以下のような img タグを罠サイトに設置
<img src="https://example.com/post?message=こんにちは" />
ユーザーは罠サイトを開いただけでメッセージを投稿してしまう
具体例 2
メッセージ投稿を POST メソッドにした
https://example.com/post
攻撃者は以下のような罠を設置
code:html
<body onload="document.forms0.submit()"> <input type="hidden" name="message"
value="こんにちは" />
</form>
</body>
ユーザーは罠サイトを開いただけでメッセージを投稿してしまう
CSRF でできること
ユーザーの権限でのリクエストの送信
スパム投稿
パスワード変更によるアカウント乗っ取り
物品の購入, 送金など夢がある
CSRF 対策
原因は他サイトから送信されたリクエストを無条件で許可してしまっていること
対策としては
トークンを用いた方法
CORS のプリフライトリクエストを用いた方法
Cookie 側の対策
CSRF トークン
ランダムなトークンを発行してセッションや Cookie に保存する
攻撃者は通常これらの値を参照・操作することができない
リクエストボディーやヘッダにもトークンを含めて, セッションや Cookie の値との一致を確認する
<input type="hidden" name="csrf_token" value="..." />
CSRF トークン
ただしトークンが攻撃者に知られないように気をつけましょう
推測不可能な乱数を使う
GET で送信するとトークンが referrer に残るので POST を使う
セッションではなく Cookie を使う場合, 同一サブドメインの Web サイトからも設定できたり, 他の脆弱性の影響を受けたりしやすいので注意が必要
CORS のプリフライトリクエスト
CORS = Cross-Origin Resource Sharing
シンプルでないリクエストを送信する場合, ブラウザは先にプリフライトリクエストでサーバーに許可を求めて, 拒否されれば送信されないことになっている
CORS のプリフライトリクエスト
サーバーは
シンプルでないリクエストを要求する
プリフライトリクエストの時点で, 不正なクロスオリジンリクエストを拒否する
の二つを実施することで, CSRF 対策となる
リクエストをシンプルでなくするためには, X-Requested-With: XMLHttpRequest のようなカスタムヘッダを付与する方法がよく使われる
Cookie 側の対策
サイトをまたいだリクエストで Cookie (セッション情報) が送信されなければ, CSRF はそもそも発生しない
Cookie の SameSite 属性を使うと, こういったサイトをまたいだ Cookie の送信を制限できる
Cookie 側の対策
SameSite 属性には以下の 3 つが設定できる
SameSite=Strict: サイトをまたいだリクエストでは送信しない
SameSite=Lax: サイトをまたいだリクエストでは, 一部を除いて送信しない
送信する例: a href, form GET
送信しない例: img, iframe, form POST
SameSite=None: 常に送信
現在は None がデフォルトの挙動だが, 今後 Lax がデフォルトになっていくとされている
その他の脆弱性 1
セッションハイジャッキング
クリックジャッキング
セッションハイジャッキング
Cookie に保存されるセッション ID を攻撃者に知られてしまい, アカウントが乗っ取られた状態になる
セッション ID が知られてしまう原因:
推測可能なセッション ID を使っている
セッション ID が URL に含まれているため, referrer 経由で外部に漏れる
セッション ID 固定化攻撃
セッション ID 固定化攻撃
攻撃者によってセッション ID を強制的に設定されてしまう
XSS やその他の脆弱性によって発生
強制されたセッション ID のまま Web サイトにログインすると, 攻撃者も同じセッション ID を使ってログインした状態になれる
セッション ID 固定化攻撃
対策:
ログイン後にセッション ID を変更する
攻撃者の知っているセッション ID は無効化
ログイン時にセッションと Cookie に推測不可能なトークンを保存し, ログイン状態の確認時にそれらの一致も確認する
攻撃者はトークンを知らないため, 確認に失敗する
そもそもログイン前にセッションを使うのを避ける
クリックジャッキング
透明な iframe を配置して, ユーザーに意図しない操作を行わせる
うまいこと押させたいボタンの位置をダミーの表示と重ねたりする
対策: フレームへの埋め込みを禁止する
X-Frame-Options: deny
その他の脆弱性 2
ユーザー入力をそのまま使ってしまうことで発生しがちな脆弱性
HTTP ヘッダインジェクション
OS コマンドインジェクション
ディレクトリトラバーサル
どれも原理は XSS や SQL インジェクションとほぼ同じ
ユーザーの入力を信用してはいけない
悪意のある入力があることを常に想定する
その他の脆弱性 3
HTTP 通信は平文のため, 中間者攻撃を受けると Cookie に保存されたセッション ID などが盗まれてしまう
HTTPS のように暗号化されたプロトコルで通信するのが望ましい
その他の脆弱性 3
うっかり HTTP でアクセスされたときは HTTPS にリダイレクトする, ということがよくある
ところが通常 Cookie は最初の HTTP でのアクセス時にも, 暗号化されずに送信されてしまう
対策としては
Cookie 側の対策
HTTP Strict Transport Security
Cookie 側の対策
Cookie に Secure 属性をつけると, HTTPS で通信する場合のみ送信されるようになる
(課題ではローカルホストで HTTP 通信のみを用いるため, 使いません)
HTTP Strict Transport Security (HSTS)
Strict-Transport-Security レスポンスヘッダを使うと HTTPS での接続を一定期間強制できる
(こちらも課題では使いません)
その他の脆弱性 4
OS, ミドルウェア, フレームワークなど自体に脆弱性が見つかることもある
継続的に情報をキャッチアップし, 対策の施されたバージョンにアップデートすることが必要
はてな社内には「セキュリティ会」という取り組みがあり, 情報の収集や共有を行っている
参考文献
Web API と GraphQL
API (Application Programming Interface)
ソフトウェアコンポーネントをプログラムから操作するためのインターフェース
API
例:
OS
ライブラリ
データベースなどのミドルウェア
ブラウザ, テキストエディタなどのアプリケーション
Web アプリケーション
ここでは Web アプリケーションが提供する API を Web API と呼ぶ
Web API の公開範囲
API の目的によって公開範囲が異なる
広く公開された API
内部的な API
広く公開された API
任意の開発者に対して仕様などが公開
特定のサービスのエコシステムの拡大や, 利便性の向上が目的
認証方法はアクセスキーなど (OAuth, etc.)
内部的な API
一般ユーザーには仕様などは非公開
アプリケーション内部で使うことが目的
フロントエンド開発のためや, サブシステム間の通信など
認証方法もフロントエンドの場合は Web サイト (HTML 配信) と共通で良い
例: ブラウザのコンソールを開いて通信の様子を見てみる
Web API の形式
フロントエンドで使われることの多い API の形式には以下のようなものがある
REST (昔からよく使われている)
GraphQL (新しい)
課題では, 明日のフロントエンド開発で使うための API を GraphQL を使って作ります
REST (Representational State Transfer)
Web API の設計スタイルのひとつ
REST に準拠した API は RESTful API や REST API と呼ばれる
ただし現実には, REST API を名乗っていても完璧に準拠していないことも多い (REST-like API)
REST の特徴
リソースを URI (= Uniform Resource Identifier) を使って区別
ステートレス
サーバーサイドでは状態を持たない
HTTP のメソッドを使ったインターフェース
GET, POST, PUT, PATCH, DELETE, ...
リソース間のリンクを辿ることができる
ブラウザでリンクを辿れるのと同じ感じ
REST-like API の例
例: ブックマークの API
一覧を取得: GET /api/bookmarks
作成: POST /api/bookmarks
取得: GET /api/bookmarks/<id>
更新: PUT /api/bookmarks/<id>
削除: DELETE /api/bookmarks/<id>
レスポンスは XML や JSON が使われることが多い (特に決まりはない)
REST Pros
ステートレスなため扱いやすい
HTTP のセマンティクスに上手く乗っているため, 理解しやすい
REST Cons
リソースという概念で表現しにくいものもある
例: 検索は GET /api/bookmarks/-/search?
REST Cons
細かい用途に合わせた API が林立してしまい, メンテナンスコストが増えがち
ブックマーク一覧: GET /api/bookmarks
ユーザー情報も合わせて: GET /api/bookmarks_with_users
重いのでデータ削減: GET /api/bookmarks_with_users?simplified=true
GraphQL
Facebook 発のクエリ言語
スキーマ
利用可能なクエリの種類や, 取得できるデータ型とその関係を定義
例:
code:resolver/schema.graphql
schema {
query: Query
...
}
type Query {
getEntry(entryId: ID!): Entry!
...
}
type Entry {
id: ID!
url: String!
title: String!
}
...
リクエスト 1
ある URL (エントリ) につけられたブックマークの一覧を取得するクエリ
code:graphql
query GetEntry {
getEntry(entryId: "4242424242") {
url
title
bookmarks {
comment
}
}
}
レスポンス 1
code:json
{
"data": {
"title": "Example Domain",
"bookmarks": [
{
"comment": "かわいい犬ですね"
},
...
]
}
}
リクエスト 2
ユーザー名も表示したくなった
code:graphql
{
getEntry(entryId: "4242424242424242") {
url
title
bookmarks {
comment
user {
name # ← 追加
}
}
}
}
レスポンス 1
code:json
{
"data": {
"title": "Example Domain",
"bookmarks": [
{
"comment": "かわいい犬ですね",
"user": {
"name": "sample"
}
},
...
]
}
}
GraphQL Pros
スキーマを使ってクライアント / サーバーの実装の一貫性を保てる
schema first な思想
後述するように, スキーマからコードや型定義を自動生成するツールがある
用途ごとに必要となるデータを, クエリの変更のみで柔軟に取得できる
用途に合わせた API を作る必要がない
GraphQL Cons
エンドポイント (リクエスト先の URL) が通常 1 個のみで, 既存の技術を利用しにくい
エンドポイントのみのアクセスログを見ても, どのようなクエリかはわからない
キャッシュをエンドポイントごとに作成することも (そのままでは) できない
重い (階層の深い) クエリが簡単に発行できてしまう
GraphQL API サーバーの実装
大まかな流れは以下のとおり
スキーマを定義
resolver を実装
HTTP サーバーに handler を組み込む
スキーマ
サーバー / クライアントの双方から独立した API の仕様
利用可能なクエリの種類や, 取得できるデータ型とその関係 (graph) を定義
スキーマの例 (Intern-Bookmark)
schema でエントリポイントとなる型を指定
code:resolver/schema.graphql
schema {
query: Query
mutation: Mutation
}
query と mutation は目的が異なる
query: 非破壊的操作 (read)
mutation: 破壊的操作 (read & write)
Query Mutation がそれぞれエントリポイントの型名
スキーマの例 (Intern-Bookmark)
エントリポイントには利用可能な操作をフィールドとして列挙
code:resolver/schema.graphql
type Query {
visitor: User!
getUser(userId: ID!): User!
getBookmark(bookmarkId: ID!): Bookmark!
getEntry(entryId: ID!): Entry!
}
スキーマの例 (Intern-Bookmark)
code:resolver/schema.graphql
type Entry {
id: ID!
url: String!
title: String!
}
スカラー型として String, Int, Boolean, ID などがある
T! は T の non-null 版
[T] は T のリスト
resolver
resolver が GraphQL のクエリを実行
スキーマで定義した type のフィールドそれぞれに対して実装する
思い出し: Query も type で定義されている
code:resolver/schema.graphql
type Query {
getEntry(entryId: ID!): Entry!
...
}
resolver
getEntry クエリに対する resolver 定義
code:resolver/resolver.go
func (r *queryResolver) GetEntry(ctx context.Context, entryID string) (*model.Entry, error) {
id, err := strconv.ParseUint(entryID, 10, 64)
if err != nil {
return nil, err
}
return r.app.FindEntryByID(id)
}
resolver
他にも type で定義した型のフィールドごとの resolver を実装
code:resolver/entry.go
func (r *entryResolver) ID(ctx context.Context, entry *model.Entry) (string, error) {
return strconv.FormatUint(entry.ID, 10), nil
}
最終的なレスポンスは, データ構造を再帰的に辿りながら resolver を実行することで作成される
handler
クエリ (リクエスト) を受け付けるハンドラを作成・登録
code:web/server.go
func (s *server) queryHandler() echo.HandlerFunc {
h := handler.GraphQL(resolver.NewExecutableSchema(
resolver.Config{
Resolvers: resolver.NewResolver(s.app),
},
))
return echo.WrapHandler(h)
}
完成
🎉
リクエストの例 (再掲)
ある URL (エントリ) につけられたブックマークの一覧を取得するクエリ
code:graphql
query GetEntry {
getEntry(entryId: "4242424242") {
url
title
bookmarks {
comment
}
}
}
レスポンスの例 (再掲)
code:json
{
"data": {
"title": "Example Domain",
"bookmarks": [
{
"comment": "かわいい犬ですね"
},
...
]
}
}
パフォーマンスの改善
単純な resolver の実装では, データ取得のパフォーマンスが良くないことがある
後述する DataLoader という仕組みを使うと改善できる
パフォーマンスが良くない例
Bookmark のフィールド user に対する resolver の単純な実装
code:resolver/bookmark.go
func (r *bookmarkResolver) User(ctx context.Context, bookmark *model.Bookmark) (*model.User, error) {
return r.app.FindUserById(bookmark.UserID)
}
パフォーマンスが良くない例
あるエントリに対する全ブックマークの, ブックマークしたユーザー名を取得するクエリ
code:graphql
{
getEntry(entryId: "4242424242424242") {
bookmarks {
user {
name
}
}
}
}
パフォーマンスが良くない例
このクエリを実行すると
エントリを引く
エントリに対するブックマークのリストを引く
各ブックマークごとにユーザーを引く
となり, 頻繁にデータベースの参照が発生して効率が悪い
いわゆる N + 1 問題
DataLoader
データ取得を一定時間遅延させてからまとめて行うことで, データベースの参照を減らす仕組み
DataLoader
r.app.FindUserById の代わりに loader の LoadUser メソッドを呼び出す
code:resolver/bookmark.go
func (r *bookmarkResolver) User(ctx context.Context, bookmark *model.Bookmark) (*model.User, error) {
ldr, err := retrieveLoader(ctx)
if err != nil {
return nil, err
}
return ldr.LoadUser(bookmark.UserID)
}
DataLoader
LoadUser は一定時間内に受け取ったユーザー ID をまとめて, データベースからユーザーを引く
app.ListUsersByIDs (WHERE id IN (?)) を使う
DataLoader
code:loader/user.go
func newUserLoader(app service.BookmarkApp) *UserLoader {
return &UserLoader{
wait: 16 * time.Millisecond,
maxBatch: 100,
fetch: func(userIDs []uint64) ([]*model.User, []error) {
xs := make([]*model.User, len(userIDs))
es := make([]error, len(userIDs))
users, _ := app.ListUsersByIDs(userIDs)
// ... (xs, es に取得したユーザーまたはエラーを代入)
return xs, es
},
}
}
参考文献
課題
必須: GraphQL API サーバーの実装
選択: Intern-Diary のセキュリティ対策
任意: GraphQL API サーバーの改良
必須: GraphQL API サーバーの実装
Intern-Diary に GraphQL API を実装してみましょう
明日のフロントエンド開発で使います
gqlgen
GraphQL API サーバー実装のためのライブラリ & コードジェネレータ
スキーマから, Go 言語のデータ型とのバインディングや resolver のモック (空の実装) を自動生成
ステップ 1. スキーマを作成
Intern-Bookmark を参考に resolver/schema.graphql を作成してみましょう
この時点では完成版である必要はありません!
ステップ 2. モデルと対応付ける
gqlgen の設定ファイル resolver/gqlgen.yml で, スキーマで定義した type と, model パッケージ内の型定義の対応を指定する
ステップ 2. モデルと対応付ける
Intern-Bookmark の場合
code:gqlgen.yml
models:
User:
model: github.com/hatena/Hatena-Intern-Universe-2019/go-Intern-Bookmark/model.User
Entry:
model: github.com/hatena/Hatena-Intern-Universe-2019/go-Intern-Bookmark/model.Entry
Bookmark:
model: github.com/hatena/Hatena-Intern-Universe-2019/go-Intern-Bookmark/model.Bookmark
ステップ 3. gqlgen でコード生成
resolver ディレクトリ以下で次のコマンドを実行
go run github.com/99designs/gqlgen
いくつかファイルが生成されます:
exec_gen.go: resolver のインターフェースや, クエリの実行に関わる実装など
resolver.go: resolver のモック (空の実装)
今後 *_gen.go は自動生成に任せて, スキーマと resolver.go のみを編集する
ステップ 4. resolver 実装準備
resolver/resolver.go を編集
code:resolver/resolver.go
//go:generate go run github.com/99designs/gqlgen
type resolver struct {
app service.BookmarkApp
}
func NewResolver(app service.BookmarkApp) ResolverRoot {
return &resolver{app}
}
1 行目は, このファイルのビルド時に gqlgen を実行する (自動生成ファイルを更新する) よう指示するコメント
ステップ 5. handler を組み込む
resolver の本実装の前に, handler を HTTP サーバーに組み込む
code:web/server.go
func (s *server) queryHandler() echo.HandlerFunc {
h := handler.GraphQL(resolver.NewExecutableSchema(
resolver.Config{
Resolvers: resolver.NewResolver(s.app),
},
))
return echo.WrapHandler(h)
}
code:web/server.go
s.e.POST("/query", s.queryHandler())
ステップ 5. handler を組み込む
合わせて GraphQL Playground を利用可能にする
code:web/server.go
func (s *server) playgroundHandler() echo.HandlerFunc {
h := handler.Playground("GraphQL Playground", "/query")
return echo.WrapHandler(h)
}
code:web/server.go
s.e.GET("/playground", s.playgroundHandler())
ステップ 6. resolver を実装
resolver/resolver.go 内の未実装部分を実装していく
panic("not implemented") のような箇所
自明なスカラー型の resolver は自動生成されているので, ID (変換が必要) と, 関連リソースを取得する部分のみ
実装を進めながら, 都度 Playground で動作確認してみましょう
Visitor のようなクエリの実装は Intern-Bookmark を参考に (resolveVisitorMiddleware が必要)
ステップ 7. 繰り返し
以下のステップを繰り返して API を完成させよう
スキーマを編集
resolver の実装を更新
Playground で動作確認
選択: セキュリティ対策を施す
講義で紹介したような脆弱性が Intern-Diary に存在するかを確認し, もしあれば対策を施してみましょう
脆弱性が存在するか, あるいはなぜ存在しないか
対策をした場合, なぜその方法で対策となるのか
(できれば) 実際に対策ができているか試してみましょう
選択: セキュリティ対策を施す
思い出しコーナー
XSS
SQL インジェクション
CSRF
セッションハイジャッキング
クリックジャッキング
どれか 1 種類以上 (ここに書いたもの以外でも OK)
任意: GraphQL API サーバーの改良
DataLoader を実装して, パフォーマンスを改善してみましょう
SQL クエリのロギングを有効にして, 改良の前後でクエリの様子を比較してみてもよい
ヒント: dataloaden
以下のようなコマンドを実行すると userloader_gen.go が自動生成される
go run github.com/vektah/dataloaden UserLoader uint64 *github.com/hatena/Hatena-Intern-Universe-2019/go-Intern-Bookmark/model.User
「uint64 型の ID で *model.User をロードする UserLoader を生成」
ヒント: dataloaden
あとは fetch 関数を書くだけで, 取得をまとめて実行する君が完成する
code:loader/user.go
//go:generate go run github.com/vektah/dataloaden UserLoader uint64 *github.com/hatena/Hatena-Intern-Universe-2019/go-Intern-Bookmark/model.User
func newUserLoader(app service.BookmarkApp) *UserLoader {
return &UserLoader{
wait: 16 * time.Millisecond,
maxBatch: 100,
fetch: func(userIDs []uint64) ([]*model.User, []error) {
xs := make([]*model.User, len(userIDs))
es := make([]error, len(userIDs))
users, _ := app.ListUsersByIDs(userIDs)
for i, userID := range userIDs {
for _, user := range users {
if user.ID == userID {
break
}
}
esi = errors.New("user not found") }
}
return xs, es
},
}
}
func (l *loaders) LoadUser(userID uint64) (*model.User, error) {
return l.userLoader.Load(userID)
}