Memo:

アイデアや気づきとかが雑に書き殴られる

N番煎じでも CSRF を説明したい

Web アプリケーションの脆弱性、特に有名なものに3つあるが CSRF だけはピンと来なかった。色々調べて理解できたのでまとめてみる。

CSRF脆弱性

CSRFCross-Site Request Forgeries脆弱性があると、利用しているサイトのユーザーに攻撃者が望む操作をさせることができる。

例えば、Aさんはあるショッピングサイトを利用しているとする。Aさんがパスワードを変更する場合はパスワード変更画面にアクセスしてそこで新しいパスワードを入力し変更ボタンを押す。

実際に行われることを細かく書くと以下のようになる。

  1. Aさんのブラウザでパスワード変更画面を開く(すでにログイン済み)
  2. 新しいパスワードを入力し、[変更する] を押す
  3. ショッピングサイトのサーバーに 新しいパスワード とAさんのリクエストであることを認証するため sesstion tokencookie)が送られる
  4. session token が正しいことを確認した上で、DB に保存されているパスワードを cA3tB9FJSdZm に更新する 1

攻撃例

このパスワード変更画面に CSRF脆弱性がある場合のことを考えてみる。
以下のような手順が成功すると、攻撃者がパスワードを任意の文字列に変更することができてしまう。

  1. 攻撃者は Aさんに攻撃用ページへのリンクと、クリックさせるような文章のメールを送る。
  2. 騙された Aさんがページを開く
  3. ページに設置されたスクリプトによって、ショッピングサイトへ「パスワードを "abc123" に変更する」リクエストが送信される。この時 Aさんのブラウザに保存された shop.jp の cookie が送信される。
  4. ショッピングサイトは session token を見て Aさんからの正規のリクエストだと判断しパスワード変更処理を行う

ここで [3] のスクリプトは以下のようなものである。ポイントはこのフォームが送信されるときに cookie (Aさんの持つ session token が含まれる)が勝手に送信されてしまうことだ。

ちなみに、AjaxXMLHttpRequest)リクエストは行っていないので 同一オリジンポリシーによる制限は受けない

<form name="form1" action="https://shop.jp/me/password/change/" method="POST">
<input type="hidden" name="password" value="abc123">
</form>
<script>document.form1.submit()</script>

これにより、攻撃者は Aさんになりすましてリクエストを偽造することができる。攻撃者のサイト経由でショッピングサイトへのリクエストを偽造することから、Cross-Site Request Forgeries と呼ばれている。

対策

IPA では以下のような根本的対策が紹介されている

  • リクエストは POST で行う(後述: SameSite=Lax にする場合は必須)
  • csrf トークンをフォームに含めて、サーバーでチェックする
  • リファラーが期待するドメインか(前述の例では shop.jp )をチェックする

安全なウェブサイトの作り方 - 1.6 CSRF(クロスサイト・リクエスト・フォージェリ):IPA 独立行政法人 情報処理推進機構

サーバー側で行う対策だけでなく、補助的に cookie を利用したクライアントサイドの対策もある。(新しいフォームの実装時に CSRF 対策を忘れてしまった場合にリスクを減らせる)

cookie には SameSite という属性がある。Strict に設定すると cookie を発行したサイトからのリクエストでのみ送信される(それ以外では送信されない)。strict では Google の検索結果からアクセスする場合や、お気に入りからアクセスした最初のリクエスト時に cookie が送られないためログインされずユーザー的に不便になる。Lax では GET リクエストのみ送信するのでバランスがいい。

ちなみに、モダンなブラウザでは Lax がデフォルトで設定される。ただし、SSO 時などの認証フロー上で cookie 送信が必要になるケースがあるようで Chrome では cookie 発効後 2分間は設定されない。( Google Developers Japan: 2020 年 2 月の SameSite Cookie の変更: 知っておくべきこと

ただし、すべてのブラウザでデフォルトが Lax に設定されているわけではないので、明示的に設定するのが安全である。(MDN 情報では Firefox, Safari はデフォルトで設定しないらしい)

繰り返しになるがブラウザに依存した対策であり、クライアントの環境によっては使えないことがあり得る。さらに、session token を必要としない処理に関しては対策とならない(例えば匿名掲示板に爆破予告を書き込ませる)。そのため補助的に使うのが良さそう。

developer.mozilla.org


  1. 実際にはセキュリティ的な観点でパスワードをハッシュ化したものを保存する

1月から12月までの英語を覚えたい

私は、1月~12月までの英語名 January ~ December を覚えるのが苦手です。

なぜ 1~12 の数字にこのような無規則な文字列が対応しているのか、そして学校や私生活ではなぜその順番を覚えること必要とされるのかが理解できません(意地が悪いのだとしか思えない!!)。

不幸なことにプログラミングでは「日付や時刻」を扱うことが多く、毎日のように目にするかもしれません。

  • 1月 January
  • 2月 February
  • 3月 March
  • 4月 April
  • 5月 May
  • 6月 June
  • 7月 July
  • 8月 August
  • 9月 September
  • 10月 October
  • 11月 November
  • 12月 December

正直なところ、上の月に関しては中学生で習うのできちんと勉強した方は覚えてるかもしれません(すごい)

しかし、やはり人は無規則なものを覚えるのが苦手だと思います。
以下はランダムに生成した文字列に1~12の番号を振ったものですが、その順番を覚えるのは難しいですよね。
(暇な人は覚えてみてください。隠したままで注釈の問題に答えられますか?→ 1

01: Ablbs,  02: Gapbw,  03: Qvatg
04: Klnaj,  05: Pzgna,  06: Isplx
07: Hizct,  08: Fpiwh,  09: Wkcij
10: Wvwpi,  11: Uqnrw,  12: Zuqzc


(以下に答えがあるので注意してください)


















もし規則やルールがあることが分かるとどうでしょうか。
子音(aiueo)に注目してみてください 文字列を見た時、何番に対応しているか言えるようになると思います。

このようなことが、月の英語名に適用できないか、良い覚え方はないかを調べてみました。

この先は、これらの記事を自分用に「覚えることを目的に」まとめたものに過ぎないので、ちゃんと知りたい方はこちらの記事をお勧めします。

www.nli-research.co.jp

www.worldfolksong.com

1~12月の覚え方

現在英語になっている月の名前は、古代ローマで一番初めに使われていた「ロムルス暦」が元になっています。この時代のローマ人はこの、農耕ベースの暦(農事暦)を使っていて、何もできない冬(現在の1, 2月)を「死の季節」と呼び名づける価値がないとされてました。

――このあたりが月を覚えるのに割と重要だと思います。

March は「死の季節」が終わりやっと暖かくなる月。農耕ベースの暦では始まりの月でした。
この季節になると、農耕がスタートしたり、ローマ軍が行動を始めるのに最適になります。

語源は戦と農耕の神である、軍神マールス(マーズ、Mars)です。

ちなみに、火星のマーズと同じ語源です。 赤い惑星というところから、火や鮮血を連想して軍神の名前が付いたそう。

「火星」という曲がありますが戦いが始まりそうな雰囲気。
これは、惑星という組曲の一番目の曲でもある。

古代ローマが盛り上がるのはこの月からです

www.youtube.com

それをベースにして、覚え方をまとめた図がこちらです。
季節や文化から連想される神の名前だったり、March から何番目かを表していたりと実は規則があり、それにいくつか変化があって今の形になったようです。

灰色で囲っている部分は個人的にイレギュラーと思う箇所です。ここさえ乗り越えれば多分記憶しやすいと思います。

暗記は嫌だ!!!という私に似たプライドの高い人でも比較的覚えやすいのではないでしょうか?

有給の半分を使うことになりましたが、
10年以上の時を経て、ようやく私は 1~12月を英語で言えるようになりました。


  1. fpiwh と klnaj はそれぞれ何番ですか?答えられたあなたは暇人の中でも記憶力が良い人です。

Rxjsの Subject を使った Pub/Sub パターン

Node.js で依存関係をうまく制御するために、Pub/Sub パターンを使いたかった。調べて見つけた Rxjs の Subject を利用することで上手く実装できたと思う。Pub/Sub を使うと何が嬉しいのかと、Subject を使った実装をまとめてみる。

例には以下の FizzBuzz クラスを使う。300ms ごとに 1~15までカウントアップして、Fizz Buzz が表示されるだけ。

そして、このクラスに対して Fizz が表示される回数を数える ことを考えてみる。

class FizzBuzz {
    static async run(): Promise<void>{
        const sleep = (ms: number) => new Promise((r) => setTimeout(() => r(null), ms));
        for(let i=1; i<=15; i++){
            await sleep(300);
            if(i % 15 === 0){
                console.log("FizzBuzz");
                continue;
            }
            if(i % 3 === 0){
                console.log("Fizz");
                continue;
            }
            if(i % 5 === 0){
                console.log("Buzz");
                continue;
            }
            console.log(i);
        }
    }
}

FizzBuzz.run();

/*
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
*/

素直に実装してみる

FizzCounter を追加して、"Fizz" を出力するタイミングでインクリメントするという感じにしてみた。問題なく動いてそうですね。

const fizzCounter = new class FizzCounter {
    private _count = 0;

    get count(): number {
        return this._count
    }

    increaseCount(): void {
        this._count++;
    }
}();

class FizzBuzz {
    static async run(): Promise<void>{
        const sleep = (ms: number) => new Promise((r) => setTimeout(() => r(null), ms));
        for(let i=1; i<=15; i++){
            await sleep(300);
            if(i % 15 === 0){
                console.log("FizzBuzz");
                continue;
            }
            if(i % 3 === 0){
                console.log("Fizz");
                fizzCounter.increaseCount();  // 👈 追加
                continue;
            }
            if(i % 5 === 0){
                console.log("Buzz");
                continue;
            }
            console.log(i);
        }
    }
}

(async () => {
    await FizzBuzz.run();
    console.log(`> Fizz count is ${fizzCounter.count}`);
})();

/*
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
> Fizz count is 4
*/

正直、シンプルなプログラムであれば問題にはならないと思うが、以下のような点が気になる。

  • fizzCounter.increaseCount() を公開しているため他の箇所からでもアクセスでき count の不整合になる可能性ある。
  • FizzBuzz クラスが自分の処理には関係ない fizzCounter.increaseCount() に依存してしまっている(無駄な結合)。
  • fizzCounter.increaseCount() のインターフェースが変わったりすると一番重要な FizzBuzz クラスが壊れる可能性がある。

これを、以下のように直したいなと思った。

  • 依存方向を FizzCounter --> FizzBuzz の方向に逆転させたい(安定方向への依存)。
  • fizzCounter が変化したとしても、FizzBuzz クラスに影響を与えないようにしたい(疎結合)。

Subject を使って書いてみる

Subject を使うことで Pub/Sub 的な形が作れる。Observable と似ているが、subscribe のタイミングや同時に publish できる数の制限はなくて融通が利く感じがする。

rxjs.dev

以下の様にすることで、依存方向が fizzCounter --> FizzBuzz になり、結合も疎になった(MQ を使った Pub/Sub には及ばないが、前のコードよりは疎結合だと思う)。

さらに、fizzCounter.increaseCount() は公開する必要がなくなったため非公開にすることができるようになった。やったー。
(テストのために公開したいということはあるかもしれない)

import { Subject } from "rxjs"

class FizzBuzz {
    static readonly fizzed = new Subject<void>();
    static async run(): Promise<void>{
        const sleep = (ms: number) => new Promise((r) => setTimeout(() => r(null), ms));
        for(let i=1; i<=15; i++){
            await sleep(300);
            if(i % 15 === 0){
                console.log("FizzBuzz");
                continue;
            }
            if(i % 3 === 0){
                console.log("Fizz");
                this.fizzed.next();  // 👈 Subject の next を呼び出す(Publish)
                continue;
            }
            if(i % 5 === 0){
                console.log("Buzz");
                continue;
            }
            console.log(i);
        }
    }
}

const fizzCounter = new class FizzCounter {
    private _count = 0;
    constructor() {
        // 👇 FizzBuzz.fizzed に Subscribe する。
        FizzBuzz.fizzed.subscribe(() => this.increaseCount());
    }

    get count(): number {
        return this._count
    }

    // 👇 private に変更した
    private increaseCount(): void {
        this._count++;
    }
}();

(async () => {
    await FizzBuzz.run();
    console.log(`> Fizz count is ${fizzCounter.count}`);
})();


/*
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
> Fizz count is 4
*/

@golevelup/nestjs-rabbitmq で実装した consumer が queue を消化できなくなった

このライブラリで作成した consumer が queue を消化できなくなり、メモリ使用率がじわじわと上昇し(300MB/8h 位の速度) OOM で停止してしまった。
RabbitMQ の GUI やサーバーのメトリクスを見ると以下のような状況になっていた。

  • 起動しているすべての consumer が Unacked メッセージを持った状態で止まっている
  • Redelivered が 1,000~2,000/s になっている
  • RabbitMQ ・ consumer 間のネットワーク通信量が大きく上がった

github.com

原因

  1. JSON として不正なメッセージを consumer が受け取る
  2. ライブラリ側のデシリアライズが失敗した際、nack されメッセージが queue の先頭に戻される
  3. requeue されたメッセージを再び consumer が受け取る
  4. [1] へ戻る、を永遠に繰り返す

このようなループを繰り返したことで queue の消化ができなくなってしまっていた。
メモリが上昇した理由は分からない。ループによるリソースの消費が速すぎて、ガベージコレクションが間に合わなかったとかかもしれない。

対策

No way to handle failed serialization (non-json) · Issue #137 · golevelup/nestjs · GitHub で説明されていた。

@golevelup/nestjs-rabbitmq@1.15.0 以上で追加された allowNonJsonMessages を true に設定することで JSON として不正なメッセージをハンドラが受け取れるようになる。それを basic.ack で消化してしまったり、basic.reject で削除したり、デッドレターキューに移動することで requeue ループを防ぐ。

@RabbitSubscribe({
    exchange,
    routingKey: [nonJsonRoutingKey],
    queue: 'subscribeQueue',
    allowNonJsonMessages: true,
})

docker container のとめかた

このような Dockerfile を $docker build . -t webapp:latest でビルドした後、$ docker run webapp:latest でコンテナを立ち上げることができる。

# Dockerfile
FROM python

WORKDIR /app

COPY . /app
RUN pip install flask

ENTRYPOINT ["bash", "entorypoint.sh"]
# entorypoint.sh
echo "Starting server..."
python3 webapp.py
# webapp.py
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello, World!"

app.run(host="0.0.0.0")

そして、その後停止するためには、$ docker stop をすれば良い。

$ docker stop b42e16eddd7d

kill-docker-container_webapp-exec_1 exited with code 137

... はずだが、すぐ終了せず、10秒経ってから強制終了(exited 137)された。

docekr stop とは?

公式ドキュメントによると、以下のように説明されている。

1つまたは複数の実行中コンテナを 停止stop します。
コンテナ内のメイン・プロセスが SIGTERM を受信し、一定期間の経過後、 SIGKILL を送信します。.... (docker stop — Docker-docs-ja 20.10 ドキュメント

SIGTERM, SIGKILLUnix / Linux でプロセスに対して送られる signal 。

SIGTERM を受け取ったプロセスは、ハンドラがあれば正常終了のための処理を行って停止という動きをできる。
プロセスに signal を送るには、kill コマンドを利用できる。

$ kill -15 { PID (Process ID) }

SIGKILL を受け取ったプロセスは、強制的に終了させられる。ちなみに、ゾンビプロセスは既に終了していることになっているため効かないらしい。もちろん、これも kill コマンドで送信できる。

$ kill -9 { PID (Process ID) }

つまり、exited 137 となったということは、SIGTERM では止まらず 10s 後、SIGKILL によって強制的に終了させられたことを表している。

コンテナ内のメイン・プロセスが SIGTERM を受信し...

docker stop コマンドの説明で引っ掛かるのはここ。docker stop コマンドを実行すると、コンテナの「メイン・プロセス」に対して SIGTERM が送られるらしい。

コンテナのメインプロセスとは何だろう?

コンテナのメインプロセス

コンテナでメインとして実行するプロセスは、 Dockerfile の最後に書かれている ENTRYPOINT か CMD か、あるいは両方によって指定します。 (コンテナ内で複数のサービスを実行 — Docker-docs-ja 19.03 ドキュメント

なるほど、 Dockerfile の一番最後のコマンドがメインプロセスとして扱われるらしい。今回作成した dockerfile では bash entorypoint.sh がメインプロセスとなるはず。

docker container 内でプロセスを見てみると、bash entorypoint.sh は PID 1 を割り当てられていた。

root@5c63c147bb2e:/app# ps 1
    PID TTY      STAT   TIME COMMAND
      1 ?        Ss     0:00 bash entorypoint.sh
root@5c63c147bb2e:/app#

--init プラグを使うと、コンテナ内で PID 1 として使われるべき init プロセスを指定できます。(Docker run リファレンス — Docker-docs-ja 20.10 ドキュメント

リファレンスを漁ってみると、このような説明があった。

メインプロセスは、init プロセスで、PID 1 が割り当てられているということだと思う。

init プロセスは UNIX / Linux のシステムで、ブートローダーによって起動したカーネルが起動するプログラムで、init が全てのプロセスを起動する。そして PID 1 が付与される。

init - Wikipedia

手元にある ubuntu で PID 1 を見ると、確かに init だった。そして /sbin/init は systemd へのシンボリックリンクだった。

$ ps 1
    PID TTY      STAT   TIME COMMAND
      1 ?        Ss     0:00 /sbin/init splash
$ ll /sbin/init 
lrwxrwxrwx 1 root root 20  110  2022 /sbin/init -> /lib/systemd/systemd*

ちなみに、このプロセスを殺そうとしても無駄だ(PID 1 は特別に保護されているらしい)

$  kill -9 1
bash: kill: (1) - Operation not permitted

$ sudo kill -9 1
(何も起きない)

container を止める

最初の docker container を止めるためには、python3 webapp.py のコマンドをメインプロセスにする必要がある。(bash が実行したコマンドは子プロセスになる)

そのためには、bash から別のコマンドを実行する際に exec を付けて実行すれば OK。

# entorypoint.sh
echo "Starting server..."
exec python3 webapp.py

確認してみると、ちゃんと PID 1 が python のコマンドになっている。

root@382c9f96490b:/app# ps 1
    PID TTY      STAT   TIME COMMAND
      1 ?        Ss     0:00 python3 webapp.py

しかし、まだちゃんと停止してくれない。
これは、前述の PID が保護されていることによるものだろう。

$ docker stop cea116b31db2

kill-docker-container_webapp-exec_1 exited with code 137

SIGTERM で終了させるためには、--init で init プロセス(メインプロセス)を PID 1 以外にしたり、SIGTERM 用のハンドラを用意したりする。

# webapp.py
from flask import Flask
import signal

def handler(signum, frame):
    print(f"signal {signum} received!")
    exit(0)
    
signal.signal(signal.SIGTERM, handler)

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello, World!"

app.run(host="0.0.0.0")

このように SIGTERM のハンドラを追加して、docker stop を実行してみるとすぐに終了した。

やったね。

$ docker stop 78a6563659b5
webapp-exec_1  | signal 15 received!
kill-docker-container_webapp-exec_1 exited with code 0

疑問

無事 docker container を止めることができた。しかし、何故メインプロセスが停止した時にコンテナが止まるのか(逆にコンテナを止めるためにメインプロセスを停止させている?メインプロセス自体がコンテナの実態?多分どちらも違う)。

逆にメインプロセスがメモリ不足で終了した際に、コンテナが止まるのはどういう仕組みなのか....

そもそもコンテナを止めるとはどういうことなのか?1
メインプロセスとコンテナの関係とは?

その謎を解明するため、我々調査隊はアマゾンの奥地へと向かった――


  1. もしかすると、私は本当の意味でコンテナを止められていないのかもしれません。

Jest でカバレッジを計測したり、CI パイプラインに組み込みたい

Jest は Meta(元 Facebook) が保守している、JavaScript のテスティングフレームワーク、らしい。Jest を使うと、コード網羅率(カバレッジ)を簡単に計測できる。

カバレッジを計測することで、テストが不足しているコードが検知できるようになって嬉しくなる。

jestjs.io

コードは長いので隠しとく

テスト対象

// add.ts
export function add(a: number, b: number): number {
    const res = a + b;
    return res;
}
// isEven.ts
export function isEven(num: number): boolean {
    let result = false;
    if (num % 2 === 0) {
        result = true;
    }
    return result;
}

export function isEven2(num: number): boolean {
    if (num % 2 === 0) {
        return true;
    }else {
        return false
    }
}

export function isEven3(num: number): boolean {
    return num % 2 === 0 ? true : false;
}
// toCSV.ts
export function toCSV(recodes: { name: string; age: number }[]): string {
    let header = "name"; header += ", age";
    const csv = recodes.map((row) => `${row.name}, ${row.age}\n`);
    return `${header}\n${csv}`;
}
// dog.ts
class Dog {
    constructor(public name: string){};

    eatBone(foods: string[]): string[] {
        return foods.filter(f => f !== "bone");
    }
}

テストコード

import { describe, it, expect } from "@jest/globals";
import { add } from "./add";
import { isEven, isEven2, isEven3 } from "./isEven";

describe("add()", () => {
    it("3 + 2 = 5", () => {
        expect(add(3, 2)).toEqual(5);
    });
});

describe("isEven", () => {
    it("8 is even", () => {
        expect(isEven(8)).toBeTruthy();
    })
})

describe("isEven2", () => {
    it("8 is even", () => {
        expect(isEven2(8)).toBeTruthy();
    })
})

describe("isEven3", () => {
    it("8 is even", () => {
        expect(isEven3(8)).toBeTruthy();
    })
})

カバレッジを計測する

--coverage オプションをつけて jest を実行すると、このような表示と Istanbul がキレイに成形した html を作ってくれる。

ファイルごとに、Stmts, Branch, Funcs, Lines メトリクスがいい感じに見れる。また、html を開くとテストコードが網羅できてない部分をマークで教えてくれる。すごく良い。

メトリクスについて

ステートメント網羅(Stmts)

ファイルに含まれる命令がどれだけテストで実行されたか。add.ts では 3つ命令があって、それが全て網羅できてる。

一番基本的なメトリクスになると思う。こちらは必ず見たい。

分岐網羅(Branch)

ファイルに含まれている分岐がどれだけテストで実行されたか。isEven.ts では分岐ルートが5つあって、3つだけ網羅できている。

ステートメント網羅だけではパスを網羅できていることまではわからないので、こちらも見ておくとベターか。ただし、分岐が一か所もないコードでは 100% になるので Branch 単体では使えない。

ちなみに、本来の分岐網羅的に isEven() には A, B のパスがある。テストでは「A」しか実行していないが、(num % 2 === 0) が false になるパターンの「B」を通っていないことを指摘されなかった。Jest では B のパスがテストできていないことをチェックできないのか??(明示的に else を書いた isEven2() ではちゃんと指摘してくれている)

関数網羅(Funcs)

ファイルに含まれている、関数やクラスのコンストラクタ、メソッド、やコールバック関数の内どれだけテストで実行されたか。一つも実行されていない(テストが無いので)

ステートメント網羅や条件網羅では、関数が網羅できていることまではわからないので、Stmts, Branch だけでは微妙そうだった場合追加で見ていいかもしれない。

行網羅(Lines)

ファイルに含まれている、命令がある行数がどれだけテストで実行されたか。ほとんどはステートメント網羅と同じ感じになるが、1行に複数の命令があると差ができる。こちらも1行も実行されていない(テストが無いので)

ステートメント網羅の方がより細かく見れるので、行網羅は気にしなくていいかも。

CI パイプラインに組み込む

coverageThreshold を設定することで、カバレッジ閾値を満たしていない場合、jest コマンドの exit code が 0 以外になるので、パイプラインに組み込んで自動でチェックするようにできる。

Jestの設定 · Jest

ただ、以下をどのように設定するべきかがちょっと難しく、コードによって調整する必要がある。

  1. どのメトリクスを使うか
  2. OK とする閾値はいくつにするか

どう設定するのが良さそうか?

軽く調べただけなので、理解がずれてるかもしれない。

1 に関して、簡単に設定するのであれば Stmts だけを見る。+で Branch を見るとより良いと思う。Funcs は何個以上網羅できてなければ失敗(閾値を負の数にすればできる)するようにすれば「一つのデカい関数はテストできてるが、ほかの小さな関数は全くテストされてない」みたいな状況でもコケさせることができる。Lines は Stmts より荒い指標なので見る必要はないと思う。

2 に関しては、下限は 60% くらい。基本的には 70~90% 位が良いみたいな意見をよく見かけた。
ミッションクリティカルなコードは 100% 近くにしてもいいかもしれないが、カバレッジ 100% というのは負担とテストの品質の効率があんまりよくない(大変な割にあまり質は上がらない)とされているっぽいので全ファイルの要求カバレッジを厳しく設定するのは辛そうだ。

追記: 実際に試行錯誤してみたところ、ちゃんとテストを書いていれば Stmts 70%~85% くらいが丁度いい閾値に感じた。75% 位であれば意識してテストコードを書けば余裕でクリアできる。

Branch を Stmts と同じレベルにすると論理的には絶対通らない(可能性は0ではないが、わざわざテストするかここ....?というレベルの if )が、TypeScript の型的にチェックを入れておく必要があるみたいな場合で引っ掛かりやすかった。なので気持ち抑えめにしておくか、Ignoring code for coverage のようなアノテーションをつけて Branch 計測しないようにするみたいなことが必要になるだろう。

Funcs は網羅できてない時点でテストケースが不足していると言えるため、より厳しく設定しても良さそうだった。厳しくしたときの副次的な効果として、一切使われていない関数を教えてくれるというメリットもあり。

感想

最近リグレッションテストが必要な場面が多くなり、ユニットテストが十分あると安心できるなーということに納得できるようになってきました。テストを書き忘れてないか(十分テストできているかまでは保証できない)を自動でチェックするために、カバレッジを CI に組み込むのはかなり有用そうだなーって思いました。

UUID v4 はなぜ暗号的に安全になってるのか(Node.js)

UUID は分散システムで一意な Identifier として使えること(= 衝突しない前提で使える)を目的にした ID で、その version 4 はランダムに作るやつ。

この前ふと、uuid v4 をパスワードのような使い方をしていいのか?が気になって調べてみた。

Secure - Cryptographically-strong random values

Node.js を良く触るが uuid モジュールでは、暗号学的に安全なランダムを使ってるよう。

ちなみに、version 7未満では Math.random() が使われることがあり、暗号的に安全じゃないかもしれないらしい(Math.random() はここで少し触れてる 疑似乱数は本当に予測できるのか試したい - Memo:

www.npmjs.com

uuid モジュールは内部的には、crypto.randomUUID() を呼んでいる。crypto は標準的な暗号機能のための API で、node.js の実装は、OpenSSL のラッパーみたいなものらしい。

Crypto | Node.js v18.3.0 Documentation

const crypto = require("crypto");
console.log(crypto.randomUUID());

randomFill() のここで乱数を作ってそう。

  const job = new RandomBytesJob(
    kCryptoJobAsync,
    buf,
    offset,
    size);

node/random.js at v16.x · nodejs/node · GitHub

更にたどっていくと...

多分ここっぽいです。OpenSSL の RAND_bytes() を呼んでますね。

bool RandomBytesTraits::DeriveBits(
    Environment* env,
    const RandomBytesConfig& params,
    ByteSource* unused) {
  CheckEntropy();  // Ensure that OpenSSL's PRNG is properly seeded.
  return RAND_bytes(params.buffer, params.size) != 0;
}

node/crypto_random.cc at v16.x · nodejs/node · GitHub

OpenSSL の wiki を眺めると RAND_bytes には software 的な乱数と、hardware 的に作られる物があるみたい。
どっちを使っているのかは分からないが、エントロピーが溜まるまで待ってるということは、hardware 的な乱数を使ってるような気はする(自信ない)。

Random Numbers - OpenSSLWiki

どこから呼んでるのかは全く分からなかったが、 SetEngine から engine を設定することで RAND_bytes がハードウェアを使ってくれるようになるみたいなので、恐らく使えるときはハードウェアを使おうとするとかなんじゃないかと想像してる。 C++ 追うの辛すぎひ。

node/crypto_util.cc at v16.x · nodejs/node · GitHub

とりあえず、node.js の uuid モジュールの v4 はパスワード的な使い方をしても問題なさそう。(※ すべての実装に暗号的強度があるとは言えないので、uuid の実装は必ず調べる必要がある)

なぜ UUID v4 の生成に暗号的乱数を使ってるのか?身長・本名や年齢・誕生日、出身地や大学、年収は?

UUID は用途上、暗号に使うわけではなく分散システムでの ID が目的なので、衝突しにくさがあれば別にセキュアである必要はない気がするけど、どうしてわざわざ暗号的に強い乱数を使って作ってるのだろう....。

調べた結果........

わかりませんでした!!

予想

「暗号論的擬似乱数生成器 の wikipedia 」の要求仕様の項目では、2つの仕様が満たす必要があるとあった。 1を満たすということは、単純に良質な乱数であるともいえる。2を満たすということは乱数に周期性や規則性が無いといってもいいと思う。

  1. 統計的無作為試験に合格するほど、無作為な乱数になってる
  2. 初期状態や途中の状態が攻撃者に明らかになっても破られないこと

この2つを満たした乱数は、「予測」し辛いだけではなく「衝突」もしにくいからという気がしてる。また、乱数に周期性があると2つの生成器の周期が重なってしまった場合、ほぼ100% 衝突するようになってしまう。2を満たしていれば周期性が無いはずなのでそのようなことも起きない。

ということは、衝突耐性を持たせるために暗号論的擬似乱数生成器を選んでいる....

ってことじゃないかと半ば無理やり納得しておわります。

ja.wikipedia.org