このノックについて
本シリーズの演習編です。本編 #1〜#4(JWT・Go基礎・GoからDB接続)まで終えてから取り組んでください。 目的に応じて使い分けられます。
- Goの基礎を復習・定着させたい方 → 前半(Q1〜Q50)。変数・構造体・関数・map・HTTPサーバー・POST/エラー処理をひと通り手を動かして固めます。
- 総合的な知識の確認や応用力を磨きたい方 → 後半(Q51〜Q100)。CTF形式で、これまで学んだ知識を「問題を解く」形で実戦的に試します。
各問題はまず自分で解いてから解答を見て、できればターミナルで動かして確認してください。間違えた問題は印をつけ、後日もう一度解きます。
環境
mkdir -p ~/go-practice/knock
cd ~/go-practice/knock
go mod init knock
各問題は main.go に書いて go run main.go で動かせます。
レベル1: 基本文法(Q1〜Q10)
Q1
"Hello, Go!" と画面に表示するプログラムを書いてください。
Q2
name という変数に文字列 "washi" を var を使って代入し、表示してください。
Q3
Q2を := を使って書き直してください。
Q4
age という整数型の変数に 30 を代入し、表示してください。
Q5
price という小数型(float64)の変数に 99.9 を代入し、表示してください。
Q6
isActive という真偽値型の変数に true を代入し、表示してください。
Q7
2つの整数 a = 10、b = 3 を宣言し、その和・差・積・商・余りをそれぞれ表示してください。
Q8
name = "washi" と age = 30 を1行で(カンマ区切りで)表示してください。
Q9
fmt.Printf を使って "名前: washi, 年齢: 30" と表示してください。
Q10
変数 count を var で宣言だけして(初期値なし)、その後 5 を代入して表示してください。宣言だけのとき初期値は何になるか確認してください。
レベル2: 構造体(Q11〜Q20)
Q11
Person という構造体を定義してください。フィールドは Name(文字列)と Age(整数)。
Q12
Q11の Person 型の変数を作り、Name="washi"、Age=30 を入れて表示してください。
Q13
Q12で作った変数から、Name だけを取り出して表示してください。
Q14
Book という構造体を定義してください。フィールドは Title(文字列)、Author(文字列)、Price(整数)。3つのフィールドに値を入れて表示してください。
Q15
Person 型を受け取って "Hello, [名前]!" という文字列を返す関数 greet を書いてください。
Q16
構造体のフィールドに、定義していない項目(例:p.Hobby)にアクセスしようとするとどうなりますか?確認してください。
Q17
Person 構造体に Email(文字列)フィールドを追加し、3フィールドすべてに値を入れてください。
Q18
2人分の Person(washiとtaro)を作り、それぞれの名前を表示してください。
Q19
構造体と配列(スライス)を組み合わせて、3人分の Person をまとめて持ち、ループで全員の名前を表示してください。
Q20
Person 型に json:"name" のようなJSONタグをつけてください。なぜタグをつけるのか説明してください。
レベル3: 関数(Q21〜Q28)
Q21
2つの整数を受け取って和を返す関数 add を書いてください。
Q22
2つの整数を受け取って、和と差の両方を返す関数 calc を書いてください(Goは複数の戻り値を返せます)。
Q23
整数を受け取って、それが偶数かどうかを真偽値で返す関数 isEven を書いてください。
Q24
文字列のスライス(複数の名前)を受け取って、全員に "さん" をつけて表示する関数を書いてください。
Q25
関数の引数 u User の User は何を表していますか?説明してください。
Q26
戻り値のない関数(何かを表示するだけ)を書いてください。
Q27
関数 divide(a, b int) を書いてください。ただし b が 0 のときはエラーを返すようにしてください(エラーの返し方を調べて実装)。
Q28
Q27で書いた divide を呼び出し、エラーが返ってきた場合とそうでない場合で処理を分けてください。
レベル4: map(Q29〜Q34)
Q29
キーが文字列、値が文字列の map を作り、"status": "ok" を入れて表示してください。
Q30
Q29のmapに後から "message": "hello" を追加してください。
Q31
キーが文字列、値が整数の map を作り、3人の名前とスコアを入れてください。特定の人のスコアを取り出して表示してください。
Q32
mapと構造体の違いを2つ説明してください。
Q33
mapからキーを削除する方法を調べて、Q30で追加した "message" を削除してください。
Q34
mapに存在しないキーにアクセスするとどうなるか確認してください。
レベル5: HTTPサーバー(Q35〜Q44)
Q35
/health にアクセスすると {"status":"ok"} を返すHTTPサーバーを書いてください(ポート8080)。
Q36
Q35のサーバーに対して、別ターミナルから curl でリクエストを送ってください。
Q37
/ping にアクセスすると {"message":"pong"} を返すエンドポイントを追加してください。
Q38
User 構造体(name, age)を使い、/user にアクセスすると {"name":"washi","age":30} を返すエンドポイントを書いてください。
Q39
w.Header().Set("Content-Type", "application/json") は何をしていますか?説明してください。
Q40
json.NewEncoder(w).Encode(data) は何をしていますか?説明してください。
Q41
http.HandleFunc("/health", healthHandler) は何をしていますか?説明してください。
Q42
http.ListenAndServe(":8080", nil) の nil は何を意味しますか?
Q43
サーバーを起動したターミナルが「止まったように見える」のはなぜですか?
Q44
1つのサーバーに /health、/ping、/user の3つのエンドポイントをまとめて実装してください。
レベル6: POST・エラー処理(Q45〜Q50)
Q45
HTTPメソッドのGET/POST/PUT/DELETEはそれぞれ何をするものですか?
Q46
/users にPOSTでJSON(name, age)を送ると、それを受け取ってそのまま返すエンドポイントを書いてください。
Q47
Q46で、リクエストのメソッドがPOST以外だった場合に405エラーを返すようにしてください。
Q48
Q46で、送られてきたJSONが不正だった場合に400エラーを返すようにしてください。
Q49
json.NewEncoder(w).Encode() と json.NewDecoder(r.Body).Decode(&data) の違いを説明してください。
Q50
Goのエラー処理の基本パターンを書き、エラーを返した後になぜ return が必要なのか説明してください。
解答・解説
各解答に問題文を併記しています(解答から先に読んでもOK)。コードはそのままコピペして
go run main.goで動く完全な形 で載せ、各解答に「何をしているか・なぜそうか」の解説をつけています。プログラムを書く問題には「▼考える順序」も付けています。補足:
:=や変数宣言はfunc main()の中でしか書けません。だから解答もfunc main()の中に書いています。「どこでも書ける」わけではない点に注意してください。
レベル1: 基本文法
Q1. "Hello, Go!" と画面に表示するプログラムを書いてください。
▼考える順序:Goの最小構成(package main → import → func main)を思い出す。表示は fmt.Println。
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
解説:Goのプログラムは必ず package main(実行可能パッケージの宣言)、import "fmt"(表示機能の読み込み)、func main()(プログラムの入口)の3点セットが要る。fmt.Println は文字列を表示して改行する。
Q2. name という変数に文字列 "washi" を var を使って代入し、表示してください。
package main
import "fmt"
func main() {
var name string = "washi"
fmt.Println(name) // washi
}
解説:var 変数名 型 = 値 が var を使う基本の書き方。name は string(文字列)型で、値は "washi"。fmt.Println(name) で中身を表示する。
Q3. Q2を := を使って書き直してください。
package main
import "fmt"
func main() {
name := "washi" // := は func の中でしか使えない
fmt.Println(name) // washi
}
解説::= はGoが右辺から型を自動推測する短縮記法。"washi" は文字列なので自動で string になる。var name string = と同じ意味だが短く書ける。ただし := は func の中でだけ使える(関数の外では var を使う)。
Q4. age という整数型の変数に 30 を代入し、表示してください。
package main
import "fmt"
func main() {
age := 30
fmt.Println(age) // 30
}
解説:30 は整数なので、:= で自動的に int(整数)型になる。数値はクォートで囲まない(囲むと文字列になる)。
Q5. price という小数型(float64)の変数に 99.9 を代入し、表示してください。
package main
import "fmt"
func main() {
price := 99.9
fmt.Println(price) // 99.9
}
解説:99.9 のように小数点を含む数は、自動で float64(小数)型になる。整数の int とは別の型。
Q6. isActive という真偽値型の変数に true を代入し、表示してください。
package main
import "fmt"
func main() {
isActive := true
fmt.Println(isActive) // true
}
解説:true / false は bool(真偽値)型。クォートで囲まない。条件分岐(if)などで使う「はい/いいえ」を表す型。
Q7. 2つの整数 a=10、b=3 を宣言し、その和・差・積・商・余りをそれぞれ表示してください。
package main
import "fmt"
func main() {
a, b := 10, 3
fmt.Println(a + b) // 13(和)
fmt.Println(a - b) // 7(差)
fmt.Println(a * b) // 30(積)
fmt.Println(a / b) // 3(商。整数同士なので小数は切り捨て)
fmt.Println(a % b) // 1(余り)
}
解説:a, b := 10, 3 のように複数の変数を1行でまとめて宣言できる。/ は割り算だが、整数同士の割り算は小数を切り捨てる(10/3 は 3.33… ではなく 3)。% は余りを求める演算子。
Q8. name="washi" と age=30 を1行で(カンマ区切りで)表示してください。
package main
import "fmt"
func main() {
name := "washi"
age := 30
fmt.Println(name, age) // washi 30
}
解説:fmt.Println はカンマ区切りで複数の値を渡せる。間は自動でスペース区切りになって表示される("washi 30")。
Q9. fmt.Printf を使って "名前: washi, 年齢: 30" と表示してください。
package main
import "fmt"
func main() {
fmt.Printf("名前: %s, 年齢: %d\n", "washi", 30)
}
解説:Printf は書式を指定して表示する。%s は文字列が入る場所、%d は整数が入る場所。後ろに渡した "washi" が %s に、30 が %d に順番に入る。Println と違い自動改行しないので、末尾に \n(改行)を付ける。
Q10. count を var で宣言だけして(初期値なし)、その後 5 を代入して表示。宣言だけのとき初期値は何になるか確認してください。
package main
import "fmt"
func main() {
var count int // 初期値なし → 0 になる
fmt.Println(count) // 0
count = 5 // 後から代入(:= ではなく = を使う。宣言済みだから)
fmt.Println(count) // 5
}
解説:Goでは値を入れずに宣言すると「ゼロ値」が入る。数値型は 0、文字列は ""(空文字)、真偽値は false。一度宣言した変数に後から入れ直すときは :=(新規宣言)ではなく =(代入)を使う。
レベル2: 構造体
Q11. Person 構造体を定義してください(Name:文字列、Age:整数)。
構造体の定義は func の外に書きます(型の定義なので)。確認用に main で使う形にしています。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "washi", Age: 30}
fmt.Println(p) // {washi 30}
}
解説:type 名前 struct { ... } で新しい型を定義する。Person は Name(文字列)と Age(整数)という2つのフィールドをまとめた型。型の定義は func の外に書く。
Q12. Q11の Person 型の変数を作り、Name="washi"、Age=30 を入れて表示してください。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "washi", Age: 30}
fmt.Println(p) // {washi 30}
}
解説:Person{Name: "washi", Age: 30} で構造体に値を入れて作る(インスタンス化)。フィールド名: 値 の形で指定する。そのまま Println すると {washi 30} のように中身が並んで表示される。
Q13. Q12で作った変数から、Name だけを取り出して表示してください。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "washi", Age: 30}
fmt.Println(p.Name) // washi
}
解説:p.Name のように「変数名.フィールド名」のドットでアクセスすると、そのフィールドの値だけを取り出せる。
Q14. Book 構造体を定義(Title, Author, Price)。3フィールドに値を入れて表示してください。
package main
import "fmt"
type Book struct {
Title string
Author string
Price int
}
func main() {
b := Book{Title: "Go入門", Author: "washi", Price: 2000}
fmt.Println(b) // {Go入門 washi 2000}
}
解説:Q11と同じ要領で、フィールドが3つの構造体を作る。Title・Author は文字列、Price は整数と、異なる型をまとめられるのが構造体の特徴。
Q15. Person 型を受け取って "Hello, [名前]!" を返す関数 greet を書いてください。
▼考える順序:入口=Person、出口=文字列。だから戻り値の型は string。中身は名前を挟んで連結。関数を定義したら main から呼んで動作確認する。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func greet(p Person) string {
return "Hello, " + p.Name + "!"
}
func main() {
p := Person{Name: "washi", Age: 30}
fmt.Println(greet(p)) // Hello, washi!
}
解説:func greet(p Person) string は「Personを受け取ってstringを返す関数」。+ で文字列を連結し、p.Name("washi")を挟んで "Hello, washi!" を作って返す。
Q16. 構造体に定義していない項目(例:p.Hobby)にアクセスするとどうなりますか?確認してください。
コンパイルエラーになります。構造体は定義したフィールドしか持てません。下のコードは わざとエラーを出す例 です(go run するとエラーで止まります)。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "washi", Age: 30}
fmt.Println(p.Hobby) // エラー: p.Hobby undefined (type Person has no field or method Hobby)
}
解説:構造体は定義時に決めたフィールドしか持てない。定義にない Hobby にアクセスすると「そんなフィールドはない」とコンパイルエラーになる。フィールドを増やしたいなら定義自体を変更するか、後から自由にキーを足せる map を使う。
Q17. Person に Email(文字列)フィールドを追加し、3フィールドすべてに値を入れてください。
package main
import "fmt"
type Person struct {
Name string
Age int
Email string
}
func main() {
p := Person{Name: "washi", Age: 30, Email: "washi@ex.com"}
fmt.Println(p) // {washi 30 washi@ex.com}
}
解説:フィールドを増やすには 構造体の定義(type部分)を書き換える。ここでは Email string を追加した。作るときも3つ全部のフィールドに値を指定する。
Q18. 2人分の Person(washiとtaro)を作り、それぞれの名前を表示してください。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
p1 := Person{Name: "washi", Age: 30}
p2 := Person{Name: "taro", Age: 25}
fmt.Println(p1.Name, p2.Name) // washi taro
}
解説:同じ Person 型から、別々の変数(p1, p2)を作れる。それぞれ独立したデータなので、p1.Name と p2.Name は別の値になる。
Q19. 構造体と配列(スライス)を組み合わせて3人分の Person を持ち、ループで全員の名前を表示してください。
▼考える順序:複数の同じ型をまとめる → スライス([]Person)。全員に同じ処理 → for range でループ。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
people := []Person{
{Name: "washi", Age: 30},
{Name: "taro", Age: 25},
{Name: "hanako", Age: 28},
}
for _, p := range people {
fmt.Println(p.Name)
}
}
解説:[]Person は「Personが複数並んだリスト(スライス)」。for _, p := range people で1人ずつ取り出して処理する。range は要素を順番に返し、_ は使わないインデックス(何番目か)を捨てる記法。出力は washi / taro / hanako が縦に並ぶ。
Q20. Person 型に json:"name" のようなJSONタグをつけてください。なぜタグをつけるのか説明してください。
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
p := Person{Name: "washi", Age: 30}
b, _ := json.Marshal(p)
fmt.Println(string(b)) // {"name":"washi","age":30}
}
解説:JSONタグは、構造体をJSONに変換するときのキー名を指定する。タグがないとフィールド名がそのまま大文字始まり(Name)で出るが、タグをつけるとAPIの慣習に合う小文字(name)になる。json.Marshal は構造体をJSONに変換する関数で、結果はバイト列なので string() で文字列に変換して表示している。
レベル3: 関数
Q21. 2つの整数を受け取って和を返す関数 add を書いてください。
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func main() {
fmt.Println(add(10, 20)) // 30
}
解説:func add(a int, b int) int は「int を2つ受け取って int を返す関数」。最後の int が戻り値の型。return a + b で和を返す。main から add(10, 20) と呼ぶと 30 が返る。
Q22. 2つの整数を受け取って、和と差の両方を返す関数 calc を書いてください(Goは複数の戻り値を返せます)。
▼考える順序:戻り値が2つ → Goは複数戻り値が書ける。型を (int, int) と並べる。呼び出し側は2つの変数で受ける。
package main
import "fmt"
func calc(a int, b int) (int, int) {
return a + b, a - b
}
func main() {
sum, diff := calc(10, 3)
fmt.Println(sum, diff) // 13 7
}
解説:戻り値が2つあるときは型を (int, int) とカッコで並べる。return a + b, a - b で2つの値をまとめて返す。受け取る側も sum, diff := と2つの変数で受ける。Goの「複数の戻り値」は、エラー処理(結果, err)でも多用される基本機能。
Q23. 整数を受け取って、それが偶数かどうかを真偽値で返す関数 isEven を書いてください。
▼考える順序:偶数=2で割った余りが0。出口は真偽値なので、n%2 == 0 の比較結果をそのまま返す。
package main
import "fmt"
func isEven(n int) bool {
return n%2 == 0
}
func main() {
fmt.Println(isEven(4)) // true
fmt.Println(isEven(7)) // false
}
解説:n%2 は2で割った余り。偶数なら余りは0。n%2 == 0 は「余りが0と等しいか」の比較で、結果は true/false になる。これをそのまま return すれば bool(真偽値)を返す関数になる。
Q24. 文字列のスライス(複数の名前)を受け取って、全員に "さん" をつけて表示する関数を書いてください。
package main
import "fmt"
func greetAll(names []string) {
for _, name := range names {
fmt.Println(name + "さん")
}
}
func main() {
greetAll([]string{"washi", "taro", "hanako"})
}
解説:[]string は文字列のリスト。for _, name := range names で1つずつ取り出し、name + "さん" で末尾に「さん」を足して表示する。出力は「washiさん」「taroさん」「hanakoさん」。この関数は表示するだけなので戻り値はなし。
Q25. 関数の引数 u User の User は何を表していますか?説明してください。
User は引数の型。「User型のデータを受け取って、関数の中では u という名前で使う」という意味。u は変数名で、User がその型。(これは説明問題なのでコードは不要)
Q26. 戻り値のない関数(何かを表示するだけ)を書いてください。
package main
import "fmt"
func sayHello() {
fmt.Println("Hello!")
}
func main() {
sayHello() // Hello!
}
解説:関数名の後の () の後ろに戻り値の型を書かなければ、何も返さない関数になる。表示やログ出力など「実行するだけで値を返す必要がない」処理に使う。
Q27. 関数 divide(a, b int) を書いてください。ただし b が 0 のときはエラーを返すようにしてください。
▼考える順序:割り算は b=0 で破綻する → 「失敗しうる処理」。Goでは失敗しうる関数は戻り値に error を足す((int, error))。先に b==0 を弾き、正常時は nil(エラーなし)を返す。
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
fmt.Println(result, err) // 5 <nil>
}
解説:0で割るとプログラムが異常終了するので、先に if b == 0 で弾く。Goの慣習では「失敗しうる関数」は戻り値の最後に error を足す。エラー時は errors.New(...) でエラーを作って返し、正常時は nil(エラーなし)を返す。errors パッケージのインポートが必要。
Q28. Q27で書いた divide を呼び出し、エラーが返ってきた場合とそうでない場合で処理を分けてください。
▼考える順序:error を返す関数は 結果, err := で受け、if err != nil で分岐。エラー処理パターンの基本形。
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("エラー:", err) // エラー: division by zero
} else {
fmt.Println("結果:", result)
}
}
解説:result, err := divide(10, 0) で結果とエラーを受け取る。if err != nil(エラーがnilでない=エラーあり)なら失敗時の処理、else なら成功時の処理に分ける。ここでは b=0 で呼んでいるのでエラーが返り、"エラー: division by zero" が表示される。これがGoのエラー処理の基本パターン。
レベル4: map
Q29. キーが文字列、値が文字列の map を作り、"status":"ok" を入れて表示してください。
package main
import "fmt"
func main() {
m := map[string]string{"status": "ok"}
fmt.Println(m) // map[status:ok]
}
解説:map[キーの型]値の型 がmapの型。map[string]string はキーも値も文字列。{"status": "ok"} で初期値を入れる。表示すると map[status:ok] の形になる。
Q30. Q29のmapに後から "message":"hello" を追加してください。
package main
import "fmt"
func main() {
m := map[string]string{"status": "ok"}
m["message"] = "hello" // 後からキーを追加できる
fmt.Println(m) // map[message:hello status:ok]
}
解説:m["キー"] = 値 で後からキーと値を追加できる。これがmapの特徴で、構造体(定義時にフィールド固定)と違って自由にキーを増やせる。
Q31. キーが文字列、値が整数の map を作り、3人の名前とスコアを入れてください。特定の人のスコアを取り出して表示してください。
package main
import "fmt"
func main() {
scores := map[string]int{
"washi": 90,
"taro": 75,
"hanako": 88,
}
fmt.Println(scores["washi"]) // 90
}
解説:map[string]int はキーが文字列、値が整数のmap(名前→スコアの対応表)。scores["washi"] のように「map名[キー]」で、そのキーに対応する値(90)を取り出せる。
Q32. mapと構造体の違いを2つ説明してください。
①構造体はフィールドが定義時に固定だが、mapはキーを後から自由に追加・削除できる。②構造体はフィールドごとに違う型を持てるが、mapは値の型が全部同じでなければならない。(説明問題なのでコードは不要)
Q33. mapからキーを削除する方法を調べて、Q30で追加した "message" を削除してください。
package main
import "fmt"
func main() {
m := map[string]string{"status": "ok", "message": "hello"}
delete(m, "message") // delete(map, キー) で削除
fmt.Println(m) // map[status:ok]
}
解説:delete(map, キー) でmapからそのキーと値を削除する組み込み関数。ここでは "message" を消したので、残りは "status" だけになる。
Q34. mapに存在しないキーにアクセスするとどうなるか確認してください。
package main
import "fmt"
func main() {
m := map[string]string{"status": "ok"}
fmt.Println(m["nothing"]) // ""(空文字。エラーにはならない)
// 存在確認する書き方
value, ok := m["nothing"]
fmt.Println(value, ok) // false
}
解説:存在しないキーにアクセスしても エラーにならず、値の型のゼロ値が返る(string なら ""、int なら 0)。これだと「値が空なのか、キーが無いのか」区別できないので、value, ok := m["key"] の2つ受け取りの形を使う。ok が false なら「キーが存在しない」と判定できる。
レベル5: HTTPサーバー
Q35. /health にアクセスすると {"status":"ok"} を返すHTTPサーバーを書いてください(ポート8080)。
▼考える順序:出口={"status":"ok"}。JSONを返す3ステップ(ヘッダー→データ→Encode)をハンドラに書き、/health に登録して起動。
package main
import (
"encoding/json"
"net/http"
)
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func main() {
http.HandleFunc("/health", healthHandler)
http.ListenAndServe(":8080", nil)
}
解説:ハンドラ関数の中で「①ヘッダーでJSONと宣言 ②データをEncodeして返す」のがJSON APIの定番。http.HandleFunc("/health", healthHandler) で「/health に来たら healthHandler を実行」と登録し、http.ListenAndServe(":8080", nil) でポート8080で起動する。
Q36. Q35のサーバーに対して、別ターミナルから curl でリクエストを送ってください。
curl http://localhost:8080/health
# → {"status":"ok"}
解説:curl URL でそのURLにGETリクエストを送る。サーバーを起動したターミナルとは別のターミナルを開いて実行する(サーバーは起動中で待機しているため)。
Q37. /ping にアクセスすると {"message":"pong"} を返すエンドポイントを追加してください。
Q35のサーバーに追加する形です。
package main
import (
"encoding/json"
"net/http"
)
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func pingHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"message": "pong"})
}
func main() {
http.HandleFunc("/health", healthHandler)
http.HandleFunc("/ping", pingHandler)
http.ListenAndServe(":8080", nil)
}
解説:Q35と同じ形のハンドラ(pingHandler)をもう1つ作り、main で http.HandleFunc("/ping", pingHandler) を追加するだけ。エンドポイントを増やすときは「ハンドラを作る → HandleFuncで登録」を繰り返す。
Q38. User 構造体(name, age)を使い、/user にアクセスすると {"name":"washi","age":30} を返すエンドポイントを書いてください。
package main
import (
"encoding/json"
"net/http"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func userHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(User{Name: "washi", Age: 30})
}
func main() {
http.HandleFunc("/user", userHandler)
http.ListenAndServe(":8080", nil)
}
解説:map ではなく構造体(User)を Encode している。JSONタグ(json:"name")のおかげで、出力は小文字キーの {"name":"washi","age":30} になる。型が決まったデータは map より構造体で返すほうが安全。
Q39. w.Header().Set("Content-Type", "application/json") は何をしていますか?説明してください。
レスポンスのヘッダーに「このレスポンスの中身はJSONですよ」と設定している。これにより受け取った側がJSONとして正しく処理できる。(説明問題なのでコードは不要)
Q40. json.NewEncoder(w).Encode(data) は何をしていますか?説明してください。
data をJSONに変換して、レスポンス(w)に書き込んでいる。Go のデータ → JSON への変換(エンコード)。(説明問題)
Q41. http.HandleFunc("/health", healthHandler) は何をしていますか?説明してください。
ルーティングの設定。/health というURLにリクエストが来たら healthHandler 関数を実行するよう紐づけている。(説明問題)
Q42. http.ListenAndServe(":8080", nil) の nil は何を意味しますか?
ルーティングの設定をカスタマイズしないという意味。nil を渡すと、http.HandleFunc で登録したデフォルトの設定が使われる。(説明問題)
Q43. サーバーを起動したターミナルが「止まったように見える」のはなぜですか?
ListenAndServe がリクエストを待ち続けるため。サーバーは止まっているのではなく、リクエストが来るのを待機している状態。Ctrl + C で止められる。(説明問題)
Q44. 1つのサーバーに /health、/ping、/user の3つのエンドポイントをまとめて実装してください。
package main
import (
"encoding/json"
"net/http"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func pingHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"message": "pong"})
}
func userHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(User{Name: "washi", Age: 30})
}
func main() {
http.HandleFunc("/health", healthHandler)
http.HandleFunc("/ping", pingHandler)
http.HandleFunc("/user", userHandler)
http.ListenAndServe(":8080", nil)
}
解説:Q35・Q37・Q38のハンドラを1つのファイルにまとめ、main で3つとも HandleFunc で登録する。1つのサーバーで複数のエンドポイントを持てる。それぞれ別のURLに別の処理が紐づく。
レベル6: POST・エラー処理
Q45. HTTPメソッドのGET/POST/PUT/DELETEはそれぞれ何をするものですか?
GET=データ取得、POST=新規作成、PUT=更新、DELETE=削除。(説明問題なのでコードは不要)
Q46. /users にPOSTでJSON(name, age)を送ると、それを受け取ってそのまま返すエンドポイントを書いてください。
▼考える順序:受け取る → Decode、返す → Encode。入れ物(User構造体)を用意して、Decodeで読み、201で返す。
package main
import (
"encoding/json"
"net/http"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
var user User
json.NewDecoder(r.Body).Decode(&user)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func main() {
http.HandleFunc("/users", usersHandler)
http.ListenAndServe(":8080", nil)
}
解説:受け取るときは json.NewDecoder(r.Body).Decode(&user)(JSON→Go)、返すときは Encode(Go→JSON)。&user で書き込み先を渡す。WriteHeader(http.StatusCreated) で201(作成成功)を返す。動作確認:curl -X POST -H "Content-Type: application/json" -d '{"name":"washi","age":30}' http://localhost:8080/users
Q47. Q46で、リクエストのメソッドがPOST以外だった場合に405エラーを返すようにしてください。
▼考える順序:正常系の前に「弾くケース」を置く。POST以外を405で落として return。
package main
import (
"encoding/json"
"net/http"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var user User
json.NewDecoder(r.Body).Decode(&user)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func main() {
http.HandleFunc("/users", usersHandler)
http.ListenAndServe(":8080", nil)
}
解説:r.Method で送られてきたメソッドが分かる。!= http.MethodPost(POSTでない)なら405を返して return で即座に止める。これを正常系より前に置くことで、許可しないメソッドを先に弾ける。
Q48. Q46で、送られてきたJSONが不正だった場合に400エラーを返すようにしてください。
▼考える順序:Decode は失敗しうる → 戻り値の err を受けてチェック。失敗なら400で return。
package main
import (
"encoding/json"
"net/http"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var user User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func main() {
http.HandleFunc("/users", usersHandler)
http.ListenAndServe(":8080", nil)
}
解説:Q46では Decode の戻り値(エラー)を捨てていたが、ここでは err := で受け取る。壊れたJSONが来ると err にエラーが入るので、if err != nil で400(Bad Request)を返して止める。外から来るデータは必ずエラーチェックする。
Q49. json.NewEncoder(w).Encode() と json.NewDecoder(r.Body).Decode(&data) の違いを説明してください。
Encode は Go のデータを JSON に変換してレスポンス(w)に書き込む(Go → JSON、送信用)。Decode はリクエスト本文(r.Body)の JSON を Go のデータに変換して読み取る(JSON → Go、受信用)。方向が逆。(説明問題なのでコードは不要)
Q50. Goのエラー処理の基本パターンを書き、エラーを返した後になぜ return が必要なのか説明してください。
パターンは以下(これは形を示す擬似コードです)。
result, err := someFunc()
if err != nil {
// エラー処理
return
}
// 正常処理
解説:return がないと、エラーを返した後も処理が続いてしまい、エラーなのに正常系の処理まで実行されてしまう。だからエラーを返したら必ず return で関数を抜ける。「処理 → エラーチェック → エラーなら止める → 大丈夫なら続ける」がGoの基本リズム。
後半(Q51〜Q100)について
ここからはCTF形式で、学んだ知識を実戦で試します。
CTF形式とは
CTF(Capture The Flag)は、問題を解くと フラグ(合言葉のような文字列) が手に入る形式です。たとえば「このトークンを改ざんして管理者になりすませ。成功するとフラグが表示される」といった問題で、実際の業務に近い技術を手を動かして攻略します。解法がすぐ分からなくても、調べてたどり着ければOK。暗記力ではなく調査力と応用力を試す形式です。
(以降のノックではこの説明は省略します)
取り組み方
後半は2種類の問題で構成しています。
- 実戦問題 … 実際にコマンドやコードを打って、フラグ・答えを自分で出す。本番のCTFに近い
- 知識確認 … 用語や仕組みを答える。答えるだけでなく「なぜそうか」を一言で言えるようにする
実戦問題は解説に「どう解いたか(手順)」を書いています。手を動かして再現してください。
本シリーズの全範囲から出題するので、まだ学んでいないテーマも含みます。各問題のマークを目安に、該当の記事を終えてから挑戦してください。
| マーク | 意味 |
|---|---|
| 【基礎】 | JWT・Go・DB(#1〜#9)で解ける |
| 【要コンテナ編】 | Docker編を終えてから |
| 【要認証認可編】 | 認証認可編を終えてから |
| 【要AWS編】 | AWS編を終えてから |
カテゴリ1: エンコード・JWT(Q51〜Q60)【基礎】
Q51〔実戦〕
以下はBase64エンコードされています。デコードしてフラグを答えてください。
RkxBR3tiYXNlNjRfaXNfbm90X2VuY3J5cHRpb259
Q52〔実戦〕
"FLAG{encode_me}" をBase64エンコードした文字列を出してください。
Q53〔実戦〕
以下のJWTから role の値を取り出してください。
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidmlld2VyIn0.abc123
Q54〔実戦〕
Q53のJWTの署名アルゴリズムを、デコードして答えてください。
Q55〔実戦〕
あるAPIはJWTの role が admin だとフラグを返します。Q53のトークンをもとに、alg:none攻撃で role: admin の偽トークンを作ってください(実際に組み立てる)。
Q56〔実戦〕
次のトークンは「ヘッダー部分」だけ抜き出したものです。これをデコードして、なぜこのトークンが危険か(どんな攻撃か)を判定してください。
eyJhbGciOiJub25lIn0
Q57〔実戦〕
http://localhost:8080/admin に、Q55で作った偽トークンを Authorization: Bearer ... ヘッダーで送るcurlコマンドを書いてください。
Q58〔知識確認〕
JWTは「暗号化」ではなく「エンコード+署名」です。では、ペイロードに秘密情報(パスワード等)を入れてはいけないのはなぜですか?
Q59〔知識確認〕
JWTトークンが漏洩しても、攻撃者が「中身を改ざんした有効なトークン」を作れないのはなぜですか(実装が正しい前提で)?
Q60〔実戦〕
以下の2つのBase64文字列をそれぞれデコードし、JWTのペイロードとして正しい(JSONとして壊れていない)のはどちらか判定してください。
A: eyJ1c2VyIjoiYWRtaW4ifQ
B: eyJ1c2VyIjoiYWRtaW4i
カテゴリ2: Go実装(Q61〜Q70)【基礎】
Q61〔実戦〕
GET /flag で {"flag":"FLAG{go_server_works}"} を返すサーバーを書いて、curlで取得してください。
Q62〔実戦〕
POST /check に {"password":"secret123"} を送り、正しければ {"flag":"FLAG{correct}"}、間違いなら401を返すサーバーを書いてください。
Q63〔実戦〕
GET /users/1 のようにURLパスの数字を取り出し、整数に変換して返すコードを書いてください。
Q64〔実戦〕
次のコードは壊れたJSONを送るとおかしな挙動をします。バグを直してください。
func handler(w http.ResponseWriter, r *http.Request) {
var user User
json.NewDecoder(r.Body).Decode(&user)
json.NewEncoder(w).Encode(user)
}
Q65〔実戦〕
/search?q=golang にアクセスされたとき、golang を取り出して {"query":"golang"} と返すコードを書いてください。
Q66〔実戦〕
次のコードはコンパイルエラーになります。エラーを再現し、原因を特定して直してください。
func main() {
name := "washi"
}
Q67〔実戦〕
環境変数 SECRET_KEY を読み取り、その値を {"key":"..."} と返すコードを書いてください。SECRET_KEY=abc123 go run main.go で起動して確認すること。
Q68〔実戦〕
リクエストの Authorization ヘッダーを取り出し、Bearer を除いたトークン部分だけを返すコードを書いてください。
Q69〔知識確認〕
パスワード照合で == ではなく subtle.ConstantTimeCompare が推奨されます。== だと何を手がかりに攻撃されますか?
Q70〔実戦〕
Q61で立てたサーバーに対し、Go側から http.Get でアクセスしてフラグを取得し、画面に表示するコードを書いてください。
カテゴリ3: SQL・DB(Q71〜Q80)【基礎】
Q71〔実戦〕
users テーブルから role が admin のユーザーを全件取得するSQLを書いてください。
Q72〔実戦〕
次のSQLに対し、name 欄に入れると認証を突破できる文字列を実際に作ってください。そのとき最終的なSQLがどう解釈されるかも示すこと。
SELECT * FROM users WHERE name = '【ここ】' AND password = '【ここ】'
Q73〔実戦〕
Q72を防ぐ、プレースホルダーを使った安全なGoのコードを書いてください。
Q74〔実戦〕
users と orders をJOINし、「各ユーザー名とその注文数」を取得するSQLを書いてください。
Q75〔実戦〕
orders テーブルの price 合計を取得するSQLを書いてください。
Q76〔実戦〕
users テーブルで WHERE email = '...' の検索が遅いです。高速化するSQL(インデックス作成)を書いてください。
Q77〔知識確認〕
送金処理「Aから1万円引く/Bに1万円足す」を、トランザクションにしないと起きうる最悪のケースを1つ挙げてください。
Q78〔実戦〕
口座テーブルで送金(A-5000, B+5000)を行うトランザクションを、BEGIN〜COMMITで書いてください。途中でやめる場合のコマンドも示すこと。
Q79〔実戦〕
UPDATE users SET role = 'admin' WHERE id = 5; を、WHEREを書き忘れるとどうなるか。実害を一言で説明し、正しい書き方を示してください。
Q80〔実戦〕
products から価格が高い順に上位3件を取得するSQLを書いてください。
カテゴリ4: コンテナ(Q81〜Q86)【要コンテナ編】
Q81〔実戦〕
動いているDockerコンテナの一覧を表示するコマンドを書いてください。
Q82〔実戦〕
myapp というコンテナの中に入って、シェルでファイルを調べるコマンドを書いてください。
Q83〔実戦〕
myimage というイメージのビルド履歴(各レイヤーの作成コマンド)を表示するコマンドを書いてください。秘密情報がここから見つかることがあります。
Q84〔実戦〕
myapp コンテナの環境変数を一覧表示するコマンドを書いてください(環境変数にフラグが入っている想定)。
Q85〔知識確認〕
Dockerfileで COPY secret.txt /app/ の後に別レイヤーで削除しても、イメージから secret.txt が取り出せてしまいます。なぜですか?
Q86〔知識確認〕
コンテナがVMより軽量で起動が速いのはなぜですか(何を共有しているか)?
カテゴリ5: 認証認可・Web(Q87〜Q94)【要認証認可編】
Q87〔知識確認・基礎〕
「ログインは通ったが管理者ページは見られない」。これは認証・認可のどちらが通って、どちらで弾かれた状態ですか?
Q88〔実戦〕
受け取ったJWTの exp(有効期限・UNIX時刻)が現在時刻を過ぎていないか検証するGoの考え方(擬似コードでよい)を書いてください。
Q89〔知識確認〕
RS256のJWTで、攻撃者が alg をHS256に変え、公開鍵を鍵として署名する攻撃を何といいますか?なぜ成立しますか?
Q90〔知識確認〕
ログインAPIで、ユーザー名が存在する場合としない場合で応答時間やメッセージが違うと、攻撃者は何ができますか?
Q91〔知識確認〕
ログイン済みユーザーに、本人の意図しないリクエストを別サイトから送らせる攻撃を何といいますか?代表的な対策を1つ。
Q92〔知識確認〕
ユーザー入力をエスケープせずHTMLに出力すると起きる攻撃を何といいますか?対策を1つ。
Q93〔実戦〕
GET /users/1 で自分の情報が見られるAPIがあります。これが脆弱かどうかを確認するには、次に何を試しますか?具体的なリクエストを書いてください。またこの脆弱性の名前は?
Q94〔知識確認〕
パスワードをDBにbcrypt等でハッシュ化して保存します。平文保存と比べ、DBが漏洩したときに何が守られますか?
カテゴリ6: 調査・総合(Q95〜Q100)
Q95〔実戦〕
あるAPIのレスポンスヘッダーにフラグが隠れているらしいです。ヘッダーも含めて表示するcurlコマンドを書いてください。
Q96〔実戦〕
APIのJSONレスポンスが1行で読みづらいです。整形して表示するコマンド(パイプで繋ぐ)を書いてください。
Q97〔実戦〕
GET /admin が403でした。アクセス制御の実装漏れを探るため、次に試すリクエストを3つ挙げてください(具体的なパスで)。
Q98〔知識確認・要AWS編〕
S3バケットのファイルが誰でも閲覧できる状態でした。これは何の設定ミスで、本来どうあるべきですか?
Q99〔実戦〕
あるAPIのレスポンスが以下でした。data をデコードして中身を答えてください。
{"data":"U0VMRUNUICogRlJPTSB1c2Vycw=="}
Q100〔実戦〕
ログインで以下のJWTが返りました。role をデコードして答え、さらにこのトークンに exp が無い場合の問題点を述べてください。
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoid2FzaGkiLCJyb2xlIjoibWVtYmVyIn0.xxxxx
解答・解説(Q51〜Q100)
各解答に問題文を併記しています。〔実戦〕は「どう解いたか」の手順つき。〔知識確認〕は答えと一言理由。Goコードはそのままコピペで
go run main.goできる完全な形 で載せています(bash・SQLはそれ単体で実行できる形)。
カテゴリ1
Q51〔実戦〕以下のBase64をデコードしてフラグを答えてください。RkxBR3tiYXNlNjRfaXNfbm90X2VuY3J5cHRpb259
echo "RkxBR3tiYXNlNjRfaXNfbm90X2VuY3J5cHRpb259" | base64 -d
# → FLAG{base64_is_not_encryption}
解説:末尾が = で終わる・英数字のみという見た目からBase64と判断 → base64 -d(decode)に通す。|(パイプ)で echo の出力を base64 に渡している。
Q52〔実戦〕"FLAG{encode_me}" をBase64エンコードした文字列を出してください。
echo -n "FLAG{encode_me}" | base64
# → RkxBR3tlbmNvZGVfbWV9
解説:echo -n の -n は末尾に改行を付けないオプション。これを忘れると改行文字までエンコードされて結果がずれる。base64(オプションなし)はエンコード。
Q53〔実戦〕次のJWTから role の値を取り出してください。eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidmlld2VyIn0.abc123
JWTは . 区切りの3パート。2番目(ペイロード)を取り出してデコードする。
echo "eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidmlld2VyIn0" | base64 -d
# → {"user":"guest","role":"viewer"}
解説:JWTは「ヘッダー.ペイロード.署名」の3つを . で繋いだもの。ユーザー情報は2番目のペイロードに入っている。デコードすると role は viewer。
Q54〔実戦〕Q53のJWTの署名アルゴリズムをデコードして答えてください。
1番目(ヘッダー)をデコードする。
echo "eyJhbGciOiJIUzI1NiJ9" | base64 -d
# → {"alg":"HS256"}
解説:署名アルゴリズムは1番目のヘッダーに alg として入っている。デコードすると HS256。
Q55〔実戦〕あるAPIはJWTの role が admin だとフラグを返します。Q53のトークンをもとに、alg:none攻撃で role: admin の偽トークンを作ってください。
▼どう考えたか:①状況把握=viewerをadminに書き換えた、通用するトークンを作る ②普通は署名でバレる ③でも問題が「alg:none」を示唆→署名検証をスキップさせる実装ミスを突ける ④ヘッダーのalgをnone、roleをadminにしてBase64して連結(署名は空)。
echo -n '{"alg":"none"}' | base64 # → eyJhbGciOiJub25lIn0=
echo -n '{"user":"guest","role":"admin"}' | base64 # → eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoiYWRtaW4ifQ==
# 組み立て(末尾の . を忘れない):
# eyJhbGciOiJub25lIn0=.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoiYWRtaW4ifQ==.
解説:JWTの署名は本来、改ざんを防ぐためにある。しかし alg を none にすると「署名なし」を意味し、脆弱なサーバーは署名検証をスキップしてしまう。その隙を突いて、ペイロードの role を admin に書き換えた偽トークンを作る。JWTは必ず3パート構造なので、署名が空でも末尾の . は必要。
▼人に説明するとどうなるか(説明の練習):「結論→状況→方針→手順→学び」の順で話すと伝わる。結論=alg:noneでroleをadminに改ざんした署名なしトークンを作った。状況=viewerのトークンで、adminなら成功する問題。方針=問題がalg:noneを示唆していたので署名検証スキップの実装ミスがあると判断。手順=algをnone・roleをadminにしてBase64連結。学び=サーバー側で許可algを固定しnoneを拒否すれば防げる。手順だけでなく「方針(なぜそう攻めたか)」を言えると、理解して解いたと伝わる。
Q56〔実戦〕次は「ヘッダー部分」だけ抜き出したものです。デコードして、なぜ危険か(どんな攻撃か)判定してください。eyJhbGciOiJub25lIn0
echo "eyJhbGciOiJub25lIn0" | base64 -d
# → {"alg":"none"}
解説:alg が none。これは署名検証を無効化させる alg:none攻撃のヘッダー。サーバーが none を受け入れると、誰でも署名なしで中身を改ざんできてしまう。だから危険。
Q57〔実戦〕http://localhost:8080/admin に、Q55で作った偽トークンを Authorization: Bearer ... で送るcurlを書いてください。
curl -H "Authorization: Bearer eyJhbGciOiJub25lIn0=.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoiYWRtaW4ifQ==." http://localhost:8080/admin
解説:-H はHTTPヘッダーを追加するオプション。JWTは Authorization: Bearer トークン の形でヘッダーに乗せて送るのが慣習。Bearer の後にQ55のトークンを置く。
Q58〔知識確認〕JWTは「暗号化」ではなく「エンコード+署名」です。ペイロードに秘密情報を入れてはいけないのはなぜですか?
ペイロードはBase64しただけで誰でもデコードして読めるから。署名は改ざん検知用で、中身を隠す機能はない。だから秘密情報(パスワード等)は入れない。
Q59〔知識確認〕JWTが漏洩しても、攻撃者が「中身を改ざんした有効なトークン」を作れないのはなぜですか(実装が正しい前提)?
署名はサーバーだけが持つ秘密鍵で作られる。攻撃者は秘密鍵を知らないので、中身を書き換えても正しい署名を再生成できず、検証で弾かれるから。
Q60〔実戦〕次の2つをデコードし、JWTペイロードとして正しい(壊れていない)のはどちらか判定してください。A:eyJ1c2VyIjoiYWRtaW4ifQ B:eyJ1c2VyIjoiYWRtaW4i
両方デコードして、JSONとして閉じているか見る。
echo "eyJ1c2VyIjoiYWRtaW4ifQ" | base64 -d # → {"user":"admin"} ← 正しい
echo "eyJ1c2VyIjoiYWRtaW4i" | base64 -d # → {"user":"admin" ← 閉じ括弧がない=壊れている
解説:JSONは { で始まり } で閉じる必要がある。A はちゃんと閉じているが、B は末尾の } が欠けていてJSONとして壊れている。正しいのは A。
カテゴリ2
Q61〔実戦〕GET /flag で {"flag":"FLAG{go_server_works}"} を返すサーバーを書いて、curlで取得してください。
▼考える順序:出口={"flag":"..."}。JSONを返す3ステップ(ヘッダー→データ→Encode)をハンドラに書き、/flagに登録→起動→curl。
package main
import (
"encoding/json"
"net/http"
)
func flagHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"flag": "FLAG{go_server_works}"})
}
func main() {
http.HandleFunc("/flag", flagHandler)
http.ListenAndServe(":8080", nil)
}
curl http://localhost:8080/flag # → {"flag":"FLAG{go_server_works}"}
解説:前半で学んだJSONを返すサーバーそのもの。ハンドラで map を Encode して返し、/flag に登録して起動。別ターミナルから curl で叩くとフラグが返る。
Q62〔実戦〕POST /check に {"password":"secret123"} を送り、正しければ {"flag":"FLAG{correct}"}、間違いなら401を返すサーバーを書いてください。
▼考える順序:①入口出口を決める(passwordを受け取り、合えばフラグ・違えば401)②弾くケースを上から(POST以外→405、壊れたJSON→400、不一致→401)③正常系は最後。正常系より先に弾くケースを並べると見通しが良い。
package main
import (
"encoding/json"
"net/http"
)
func checkHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var body struct {
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
if body.Password != "secret123" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"flag": "FLAG{correct}"})
}
func main() {
http.HandleFunc("/check", checkHandler)
http.ListenAndServe(":8080", nil)
}
curl -X POST -H "Content-Type: application/json" -d '{"password":"secret123"}' http://localhost:8080/check
# → {"flag":"FLAG{correct}"}
解説:struct{ Password string } はその場限りの小さな構造体(受け取り専用)。弾くケース(メソッド違い→405、壊れJSON→400、パス不一致→401)を上から並べ、全部通過したら正常系でフラグを返す。http.StatusUnauthorized は401。
Q63〔実戦〕GET /users/1 のようにURLパスの数字を取り出し、整数に変換して返すコードを書いてください。
▼考える順序:URLから取れる値は必ず文字列("1")。だから「文字列で受け取る→整数に変換」の2段階。変換は失敗しうるのでエラーを受けて弾く。
package main
import (
"encoding/json"
"net/http"
"strconv"
)
func userHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id") // パスの値は文字列で届く("1")
id, err := strconv.Atoi(idStr) // 文字列→整数に変換
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(map[string]int{"id": id})
}
func main() {
http.HandleFunc("/users/{id}", userHandler) // {id} でパスの一部を変数として受け取る
http.ListenAndServe(":8080", nil)
}
curl http://localhost:8080/users/1 # → {"id":1}
解説:r.PathValue("id") でURLパスの {id} 部分を取り出す(Go 1.22以降の機能)。取れるのは必ず文字列なので、strconv.Atoi(ASCII to integer)で整数に変換する。変換は失敗しうる("abc"などが来たら)ので err をチェックして400で弾く。strconv のインポートが必要。
Q64〔実戦〕次のコードは壊れたJSONを送るとおかしくなります。バグを直してください。
問題のコード:
func handler(w http.ResponseWriter, r *http.Request) {
var user User
json.NewDecoder(r.Body).Decode(&user)
json.NewEncoder(w).Encode(user)
}
▼考える順序:①壊れたJSONをPOSTして挙動を観察 ②外部データを扱う箇所(Decode)を疑う ③Decodeのerrを受けていない=失敗を握りつぶしている、と気づく ④if err != nilで弾く。
直したコード(実行可能な完全形):
package main
import (
"encoding/json"
"net/http"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func handler(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(user)
}
func main() {
http.HandleFunc("/users", handler)
http.ListenAndServe(":8080", nil)
}
解説:元のバグは Decode の戻り値(エラー)を受け取らずに捨てていたこと。壊れたJSONが来ても気づかず、空のまま処理が進んでしまう。if err := ...; err != nil の形でエラーを受けてチェックし、失敗なら400で弾く。「err を返す関数は必ず受けてチェック」がGoの基本。
Q65〔実戦〕/search?q=golang にアクセスされたとき、golang を取り出して {"query":"golang"} と返すコードを書いてください。
▼考える順序:?key=valueはクエリパラメータ。パスの値(PathValue)とは取り出し方が違う。?以降はQuery().Get。
package main
import (
"encoding/json"
"net/http"
)
func searchHandler(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q") // ?q=... の値を取り出す
json.NewEncoder(w).Encode(map[string]string{"query": q})
}
func main() {
http.HandleFunc("/search", searchHandler)
http.ListenAndServe(":8080", nil)
}
curl "http://localhost:8080/search?q=golang" # → {"query":"golang"}
解説:URLの ?q=golang の部分はクエリパラメータと呼ぶ。パスの一部(/users/1 の 1)とは取り出し方が違い、r.URL.Query().Get("q") で q の値を取る。curlで ? を含むURLはクォートで囲む(シェルが特殊扱いするため)。
Q66〔実戦〕次のコードはコンパイルエラーになります。エラーを再現し、原因を特定して直してください。
問題のコード:
func main() {
name := "washi"
}
原因を確認するため実際に動かすと:
package main
func main() {
name := "washi" // この name をどこでも使っていない
}
go run main.go
# → ./main.go:4:2: declared and not used: name
直し方(使うか、消す):
package main
import "fmt"
func main() {
name := "washi"
fmt.Println(name) // 使えばエラーが消える
}
解説:Goは「宣言したのに使っていないローカル変数」をコンパイルエラーにする厳しい言語。エラーメッセージ declared and not used: name が原因そのもの。直し方は「その変数を使う」か「宣言を消す」。当てずっぽうで直さず、まずエラーメッセージを読むのがコツ。同じ理由で「使わないimport」もエラーになる。
Q67〔実戦〕環境変数 SECRET_KEY を読み取り、{"key":"..."} と返すコードを書いてください。SECRET_KEY=abc123 go run main.go で確認すること。
▼考える順序:環境変数は os.Getenv("名前") で読む。起動時はコマンドの頭に 名前=値 を付けて渡す。なぜ環境変数か=秘密鍵をコードに直書きするとGitに残って漏洩するから。
package main
import (
"encoding/json"
"net/http"
"os"
)
func keyHandler(w http.ResponseWriter, r *http.Request) {
key := os.Getenv("SECRET_KEY") // 環境変数を読む
json.NewEncoder(w).Encode(map[string]string{"key": key})
}
func main() {
http.HandleFunc("/key", keyHandler)
http.ListenAndServe(":8080", nil)
}
SECRET_KEY=abc123 go run main.go
# 別ターミナルで:
curl http://localhost:8080/key # → {"key":"abc123"}
解説:os.Getenv("名前") で環境変数の値を読む。起動コマンドの頭に SECRET_KEY=abc123 を付けると、そのプロセスにだけ環境変数が渡る。秘密鍵をコードに直書きするとGitに残って漏洩するので、環境変数から読むのが基本。os のインポートが必要。
Q68〔実戦〕リクエストの Authorization ヘッダーを取り出し、Bearer を除いたトークン部分だけを返すコードを書いてください。
▼考える順序:ヘッダーはr.Header.Get("Authorization")。値はBearer xxxの形なので、先頭のBearer をstrings.TrimPrefixで削る。「値がどういう形か」を先に把握すると処理が見える。
package main
import (
"encoding/json"
"net/http"
"strings"
)
func tokenHandler(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization") // "Bearer xxx.yyy.zzz"
token := strings.TrimPrefix(auth, "Bearer ") // "xxx.yyy.zzz"
json.NewEncoder(w).Encode(map[string]string{"token": token})
}
func main() {
http.HandleFunc("/token", tokenHandler)
http.ListenAndServe(":8080", nil)
}
curl -H "Authorization: Bearer abc.def.ghi" http://localhost:8080/token
# → {"token":"abc.def.ghi"}
解説:r.Header.Get("Authorization") でリクエストヘッダーの値を取る。値は Bearer トークン の形なので、strings.TrimPrefix(文字列, "Bearer ") で先頭の Bearer を削ってトークン部分だけにする。strings のインポートが必要。
Q69〔知識確認〕パスワード照合で == ではなく subtle.ConstantTimeCompare が推奨されます。== だと何を手がかりに攻撃されますか?
不一致の位置で比較を打ち切るため、一致が長いほど処理時間が伸びる。攻撃者はその時間差を測り、正しい値を先頭から1文字ずつ推測できる(タイミング攻撃)。
Q70〔実戦〕Q61で立てたサーバーに、Go側から http.Get でアクセスしてフラグを取得し、画面に表示するコードを書いてください。
▼考える順序:今回は「送る側(クライアント)」。受ける側のHandleFuncではなくhttp.Getを使う。①GET送信 ②エラー確認 ③本文を閉じる予約(defer)④本文を読む ⑤文字列化して表示。
package main
import (
"fmt"
"io"
"log"
"net/http"
)
func main() {
resp, err := http.Get("http://localhost:8080/flag") // GETリクエストを送る
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close() // 使い終わったら閉じる予約
body, _ := io.ReadAll(resp.Body) // 本文を全部読む
fmt.Println(string(body)) // → {"flag":"FLAG{go_server_works}"}
}
解説:これまではサーバー(受ける側)を書いてきたが、今回は クライアント(送る側)。http.Get(URL) でGETリクエストを送り、レスポンスを受け取る。defer resp.Body.Close() で本文を確実に閉じ、io.ReadAll で中身を全部読む。読んだ結果はバイト列なので string() で文字列化して表示。Q61のサーバーを起動した状態で実行する。
カテゴリ3
Q71〔実戦〕users テーブルから role が admin のユーザーを全件取得するSQLを書いてください。
SELECT * FROM users WHERE role = 'admin';
解説:SELECT * は全カラム取得、WHERE role = 'admin' で role が admin の行だけに絞る。文字列はシングルクォートで囲む。
Q72〔実戦〕次のSQLに対し、name 欄に入れると認証突破できる文字列を作り、最終的なSQLがどう解釈されるか示してください。SELECT * FROM users WHERE name = '【ここ】' AND password = '【ここ】'
name 欄に ' OR '1'='1' -- を入れる。最終的なSQL:
SELECT * FROM users WHERE name = '' OR '1'='1' --' AND password = '...'
解説:'1'='1' が常に真になり、-- 以降がコメント化してパスワード条件が無効になる。結果、全件ヒットして認証突破。手順:入力がSQLに直結している箇所を見つける → クォートを閉じて OR '1'='1' を注入 → 残りを -- で無効化。これがSQLインジェクション攻撃の典型。
Q73〔実戦〕Q72を防ぐ、プレースホルダーを使った安全なGoのコードを書いてください。
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
func main() {
db, _ := sql.Open("postgres", "host=localhost port=5432 user=postgres password=password dbname=testdb sslmode=disable")
name := "washi"
password := "secret"
// $1, $2 は値をデータとして渡す穴(プレースホルダー)
row := db.QueryRow("SELECT * FROM users WHERE name = $1 AND password = $2", name, password)
fmt.Println(row)
}
解説:$1 $2 はプレースホルダーで、値を「ただのデータ」としてDBに渡す。入力がSQL命令として解釈されないので、' OR '1'='1' を入れられても文字列扱いになり注入を防げる。文字列連結でSQLを組み立てるのは厳禁。(#4で学んだDB接続が前提)
Q74〔実戦〕users と orders をJOINし、「各ユーザー名とその注文数」を取得するSQLを書いてください。
SELECT users.name, COUNT(orders.id)
FROM users
LEFT JOIN orders ON users.id = orders.user_id
GROUP BY users.name;
解説:LEFT JOIN で注文0件のユーザーも含める(INNER JOINだと注文ゼロの人が消える)。GROUP BY users.name でユーザーごとにまとめ、COUNT で各人の注文数を数える。
Q75〔実戦〕orders テーブルの price 合計を取得するSQLを書いてください。
SELECT SUM(price) FROM orders;
解説:SUM() は合計を出す集計関数。他に件数の COUNT()、平均の AVG()、最大の MAX()、最小の MIN() がある。
Q76〔実戦〕users で WHERE email = '...' の検索が遅いです。高速化するSQL(インデックス作成)を書いてください。
CREATE INDEX idx_users_email ON users (email);
解説:よく検索するカラム(email)にインデックスを貼ると、全行を1つずつ調べるフルスキャンを避けられて検索が速くなる。インデックス名(idx_users_email)は分かりやすい慣習的な付け方。
Q77〔知識確認〕送金「Aから1万円引く/Bに1万円足す」を、トランザクションにしないと起きうる最悪のケースを1つ挙げてください。
「Aから引く」だけ成功して「Bに足す」前に落ちると、お金が消える(どこにも存在しなくなる)。だから2つの操作は1つのトランザクションにまとめ、両方成功か両方失敗にする必要がある。
Q78〔実戦〕口座テーブルで送金(A-5000, B+5000)を行うトランザクションをBEGIN〜COMMITで書いてください。途中でやめる場合のコマンドも示すこと。
BEGIN;
UPDATE accounts SET balance = balance - 5000 WHERE name = 'A';
UPDATE accounts SET balance = balance + 5000 WHERE name = 'B';
COMMIT; -- 途中でやめるなら COMMIT の代わりに ROLLBACK;
解説:BEGIN でトランザクション開始、COMMIT で確定する。BEGIN〜COMMITの間の操作は「全部成功か、全部なかったことにするか」のどちらかになる。途中で問題があれば ROLLBACK を使うと、BEGIN以降の変更が全部取り消される。これでQ77の「片方だけ成功」を防げる。
Q79〔実戦〕UPDATE users SET role = 'admin' WHERE id = 5; でWHEREを書き忘れるとどうなるか。実害を一言で説明し、正しい書き方を示してください。
WHEREを忘れると全ユーザーが admin になる(全員管理者)。正しくは必ずWHEREで対象を限定:
UPDATE users SET role = 'admin' WHERE id = 5;
解説:UPDATEやDELETEで WHERE を忘れると、テーブルの全行が対象になる。UPDATE users SET role='admin'(WHEREなし)は全員をadminにしてしまう重大事故。UPDATE/DELETEを書くときは必ずWHEREで対象を絞る。
Q80〔実戦〕products から価格が高い順に上位3件を取得するSQLを書いてください。
SELECT * FROM products ORDER BY price DESC LIMIT 3;
解説:ORDER BY price DESC で価格の降順(高い順)に並べる(DESC=descending)。LIMIT 3 で上から3件だけ取る。昇順にしたいなら ASC(省略時は昇順)。
カテゴリ4
Q81〔実戦〕動いているDockerコンテナの一覧を表示するコマンドを書いてください。
docker ps # 動いているコンテナ
docker ps -a # 停止中も含める
解説:docker ps(process status)で起動中のコンテナ一覧が見える。-a(all)を付けると停止中のものも含めて全部表示する。
Q82〔実戦〕myapp というコンテナの中に入って、シェルでファイルを調べるコマンドを書いてください。
docker exec -it myapp sh # bashがあれば sh の代わりに bash
解説:docker exec は動いているコンテナの中でコマンドを実行する。-it(-i=対話的, -t=端末)でシェルを操作できる状態にし、sh でシェルを起動。中に入ったら ls や cat でファイルを調べられる。
Q83〔実戦〕myimage のビルド履歴(各レイヤーの作成コマンド)を表示するコマンドを書いてください。秘密情報がここから見つかることがあります。
docker history myimage
解説:docker history はイメージが作られた各レイヤーの作成コマンドを表示する。COPY や ENV で秘密情報を埋め込んでいると、ここから見つかってしまう(後で削除しても前のレイヤーに残る)。
Q84〔実戦〕myapp コンテナの環境変数を一覧表示するコマンドを書いてください(環境変数にフラグが入っている想定)。
docker exec myapp env # コンテナ内で env コマンドを実行
# または:
docker inspect myapp # の出力の "Env" 項目を見る
解説:docker exec myapp env で、コンテナ内の env コマンド(環境変数一覧表示)を実行する。docker inspect でコンテナの詳細設定を見て、その中の Env 項目を確認する方法もある。環境変数にAPIキーやフラグを入れていると、ここから読み取れてしまう。
Q85〔知識確認〕Dockerfileで COPY secret.txt /app/ の後に別レイヤーで削除しても、イメージから secret.txt が取り出せてしまいます。なぜですか?
イメージはレイヤーの積み重ね。追加レイヤーと削除レイヤーは別物で、前のレイヤーにファイルが残るから。秘密は最初からイメージに入れないのが正解。
Q86〔知識確認〕コンテナがVMより軽量で起動が速いのはなぜですか(何を共有しているか)?
VMはOSごと仮想化するが、コンテナはホストOSのカーネルを共有してプロセスを隔離するだけだから。OSを丸ごと起動しない分、軽くて速い。
カテゴリ5
Q87〔知識確認〕「ログインは通ったが管理者ページは見られない」。認証・認可のどちらが通って、どちらで弾かれた状態ですか?
認証(ログイン=誰か)は通った。認可(権限=何をしてよいか)で弾かれた状態。認証=本人確認、認可=権限チェック、と区別する。
Q88〔実戦〕受け取ったJWTの exp(有効期限・UNIX時刻)が現在時刻を過ぎていないか検証するGoの考え方(擬似コードでよい)を書いてください。
▼考える順序:現在時刻(UNIX秒)と exp を比較し、過ぎていたら拒否。署名検証だけでなくこの期限チェックも必須。
package main
import (
"errors"
"fmt"
"time"
)
// claims.Exp は JWT の exp(有効期限・UNIX秒)が入っている想定
func validateExp(exp int64) error {
if time.Now().Unix() > exp {
return errors.New("token expired") // 現在時刻が exp を過ぎている → 期限切れ
}
return nil
}
func main() {
// 例: 過去の時刻(1日前)を渡すと期限切れになる
past := time.Now().Add(-24 * time.Hour).Unix()
fmt.Println(validateExp(past)) // token expired
}
解説:time.Now().Unix() で現在時刻をUNIX秒(1970年からの経過秒数)で取得し、JWTの exp と比較する。現在 > exp なら期限切れなのでエラーを返す。JWTは署名検証だけでなく、この有効期限チェックも必須(署名が正しくても期限切れなら拒否する)。
Q89〔知識確認〕RS256のJWTで、攻撃者が alg をHS256に変え、公開鍵を鍵として署名する攻撃を何といいますか?なぜ成立しますか?
アルゴリズム混同攻撃(key confusion)。公開鍵は誰でも入手でき、それをHS256の鍵として署名すると、サーバーが公開鍵で検証して通してしまうため。対策は許可algをサーバー側で固定すること。
Q90〔知識確認〕ログインAPIで、ユーザー名が存在する場合としない場合で応答時間やメッセージが違うと、攻撃者は何ができますか?
ユーザー名が存在するか否かを判別できる(ユーザー列挙)。存在するアカウントを絞り込んで攻撃できる。対策は、応答内容・応答時間を統一すること。
Q91〔知識確認〕ログイン済みユーザーに、本人の意図しないリクエストを別サイトから送らせる攻撃を何といいますか?代表的な対策を1つ。
CSRF(クロスサイトリクエストフォージェリ)。対策:CSRFトークン、SameSite Cookie属性など。
Q92〔知識確認〕ユーザー入力をエスケープせずHTMLに出力すると起きる攻撃を何といいますか?対策を1つ。
XSS(クロスサイトスクリプティング)。対策:出力時のHTMLエスケープ。
Q93〔実戦〕GET /users/1 で自分の情報が見られるAPIがあります。脆弱か確認するには次に何を試しますか?具体的なリクエストを書いてください。脆弱性の名前は?
自分のID以外を試す。
curl http://localhost:8080/users/2
curl http://localhost:8080/users/3
解説:自分が id=1 なら、2 や 3 を試して他人の情報が見えるか確認する。見えたら脆弱で、名前は IDOR(安全でない直接オブジェクト参照)。対策はリソースごとに「これは本当にこの人のものか」という認可チェックを入れること。
Q94〔知識確認〕パスワードをbcrypt等でハッシュ化して保存します。平文保存と比べ、DBが漏洩したときに何が守られますか?
元のパスワードが守られる。ハッシュは元に戻せないので、DBが漏れても平文パスワードは分からない(特にbcryptは総当たりも遅くて困難)。
カテゴリ6
Q95〔実戦〕あるAPIのレスポンスヘッダーにフラグが隠れているらしいです。ヘッダーも含めて表示するcurlコマンドを書いてください。
curl -i http://localhost:8080/... # -i でヘッダーも表示
# curl -v ならリクエスト/レスポンスの詳細も見える
解説:通常の curl は本文しか表示しないが、-i(include)を付けるとレスポンスヘッダーも表示される。ヘッダーに隠れた X-Flag: のようなカスタムヘッダーを探せる。-v(verbose)はさらに詳しい通信内容を出す。
Q96〔実戦〕APIのJSONレスポンスが1行で読みづらいです。整形して表示するコマンド(パイプで繋ぐ)を書いてください。
curl http://localhost:8080/... | jq
解説:jq はJSONを整形・加工するコマンド。|(パイプ)でcurlの出力をjqに渡すと、インデント付きの読みやすい形に整形される。特定のキーだけ取り出す(jq '.name' など)こともできる。
Q97〔実戦〕GET /admin が403でした。アクセス制御の実装漏れを探るため、次に試すリクエストを3つ挙げてください(具体的なパスで)。
大文字小文字・末尾スラッシュ・別表記を試す。
curl http://localhost:8080/Admin # 大文字
curl http://localhost:8080/admin/ # 末尾スラッシュ
curl http://localhost:8080/admin%2f # URLエンコード
解説:アクセス制御が「/admin」という文字列の完全一致だけで判定していると、表記を少し変えた別のパス(大文字・末尾スラッシュ・エンコード)で制御をすり抜けられることがある。同じ場所を指すのに制御が効かない、という実装漏れを探る。
Q98〔知識確認〕S3バケットのファイルが誰でも閲覧できる状態でした。これは何の設定ミスで、本来どうあるべきですか?
S3バケットの公開設定(パブリックアクセス許可)のミス。本来は必要な相手だけに限定する最小権限であるべき。
Q99〔実戦〕あるAPIのレスポンスが以下でした。data をデコードして中身を答えてください。{"data":"U0VMRUNUICogRlJPTSB1c2Vycw=="}
data の値(Base64文字列)をデコードする。
echo "U0VMRUNUICogRlJPTSB1c2Vycw==" | base64 -d
# → SELECT * FROM users
解説:data の値が末尾 ==・英数字でBase64と分かる。デコードするとSQL文 SELECT * FROM users が出てくる。APIが内部のSQLをBase64で隠して返している(が、Base64は暗号化ではないので簡単に読める)状況。
Q100〔実戦〕ログインで以下のJWTが返りました。role をデコードして答え、さらにこのトークンに exp が無い場合の問題点を述べてください。eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoid2FzaGkiLCJyb2xlIjoibWVtYmVyIn0.xxxxx
ペイロード(2番目)をデコードして role を見る。
echo "eyJ1c2VyIjoid2FzaGkiLCJyb2xlIjoibWVtYmVyIn0" | base64 -d
# → {"user":"washi","role":"member"}
解説:role は member。exp(有効期限)が無いと、トークンが永久に有効になってしまう。一度漏洩すると無効化できず、攻撃者がいつまでも使い続けられる。対策:トークンには必ず exp を持たせ、サーバー側で期限切れを検証する(Q88の仕組み)。
全100問。〔実戦〕は手を動かして答えを出す。〔知識確認〕は理由まで言えるようにする。