りゅうじのプログラミング学習blog

初学者による初学者の為の記事です。

2022年4月18日 Node.js express csrf対策

csurfのパッケージを使用してcsrf対策をします。

注: パッケージ名はcsurf となる点に注目してください。csrf というパッケージも存在します。

csurf

http://expressjs.com/en/resources/middleware/csurf.html

csrf

https://github.com/pillarjs/csrf

実装

前提

  • テンプレートはejsを使用します。
  • Expressのバージョン14.6.0以降を使用します。(body-parserは標準搭載されているのでインストールは不要)
  • 今回はcsurfの実装をメインなのでexpress等、前段階で必要なパッケージのインストールは割愛します。

csurfはクッキーもしくはセッションを利用します。

今回はセッションで行う方法をアウトプットします。

まず必要なパッケージをインストールしましょう。

yarn add csurf
yarn add express-session

ログイン機能への実装例です。ログインはRouterを使ってモジュール化しています。(express-sessionのミドルウェアは本体のapp.jsに実装しているためここでは記述はありませんが割愛します)

login.js

const router = require("express").Router();
const db = require("../models");
const bcrypt = require("bcrypt");
const csrf = require("csurf");
const csrfProtection = csrf();

router.get("/", csrfProtection, (req, res) => {
  res.render("login", { csrfToken: req.csrfToken() });
});

router.post("/", csrfProtection, async (req, res) => {
  const email = req.body.email;
  const authentication = await db.User.findOne({ where: { email: email } });
  const normalPassword = req.body.password;
  const hashedPassword = authentication?.password;
  bcrypt.compare(normalPassword, hashedPassword, (error, passwordEqual) => {
    if (authentication && passwordEqual) {
      req.session.userId = authentication.id;
      req.session.userName = authentication.name;
      if (req.session.url) {
        res.redirect(req.session.url);
      } else {
        res.redirect("/index");
      }
    } else {
      res.redirect("/login");
    }
  });
});

module.exports = router;
  • const csrf = require('csurf'); でインストールしています。
  • const csrfProtection = csrf() でCSRF対策のためのミドルウェアを作ります。今回はクッキーではなくセッションにトークンを保持してCSRF対策をしています。引数を渡さないとデフォルトでセッションを設定した事になるのでここでは引数はなしです。(クッキーを使用する場合は引数で設定します)
  • csrfProtection をCSRF対策を施したい各ルートに対して埋め込みます。
  • トークンは req.csrfToken() で取得できるので、これをテンプレートにパラメータとして渡してフォームに埋め込むためにres.render の第二引数に持たせています。

login.ejs

<!DOCTYPE html>
<html lang="jp">
<head>
  <%- include("./_share/meta.ejs") %> 
  <title>Document</title>
</head>
<body>
  <h1>ログイン画面</h1>
    <form action="/login" method="post">
    <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
    <p>メールアドレス</p>
    <input type="text" name="email">
    <p>パスワード</p>
    <input type="password" name="password">
    <input type="submit" value="ログイン">
    <a href="/signup">ユーザー登録画面へ</a>
    <a href="/index">一覧ページへ</a>
    <a href="/passwordReset">パスワードをお忘れの方はこちら</a>
</body>
</html>

formタグの一つ下のinputタグに注目してください。

  • type=”hidden” とすることで見えないフォームを生成しています。
  • value=”<%= csrfToken %>” ここで先程、res.render の第二引数で持たせたトークンをフォームに埋め込んでいます。

こちらを検証ツールで確認してみます。

トークンの値を変更するとpostリクエストはエラーになり、実装したフォームからでしかpostリクエストは通らないよう実装でき、csrf対策ができている事を確認できるかと思います。

参考

csurf

2022年3月27日 Node.js CSRFパッケージ

CSRF対策

  1. 書き込みできるドメインを限定する
  2. 適切な画面遷移でデータ登録されている事を確認する(トークン制御を入れる)
  3. キャプチャ(画面認証を入れる)

今回は2の対策をしていきます。

パッケージ導入

yarn add csrf
  • ミドルウェアとして組み込みではなく、必要な箇所へ実装していくものです。

具体的な実装方法

secretとtokenの生成と検証

const Tokens = require('csrf');
const tokens = new Tokens();

(async function () {
  // 発行
  const secret = await tokens.secret();
  const token = tokens.create(secret);

  // 検証
  if (!tokens.verify(secret, token)) {
    throw new Error("invalid token!");
  }
})();
  • secretはサーバー保持(セッション)
  • tokenはクライアント返却(クッキー)

この2つの組み合わせが正しくないとNGだという仕組みがCSRF対策です。

データ登録の直前に検証をします。

例えば登録→確認→完了という遷移をする際に、登録→確認の遷移時(登録の内容をサーバにリクエストする時)にそのリクエストが正当なものか?を検証しても意味ない(あくまで、確認→完了の遷移の際(データ登録時)にそのリクエストが正当である事を確かめる必要がある)

Tokensクラス コンストラク

引数 options

  • saltLength 内部利用するsalt長さ。デフォルト 8。
  • secretLength secret長さ。デフォルト18。

戻り値

TokensクラスAPI

secret()

  • 構文 tokens.secret(callback) サーバー保存するsecretの生成
    • 引数 callback (err, secret) ⇒ {} secret生成後に呼び出される関数
    • 戻り値 引数がない場合はPromise

    create()

    • 構文 tokens.create(secret) クライアントへ戻す tokenの生成
    • 引数 secret secret()で生成されたsecret文字列
    • 戻り値 token文字列

    verify()

    • 構文 tokens.verify(secret, token) 与えられたsecretとtokenの組み合わせが正しいか検証
    • 引数 secret 生成したsecret token 生成したtoken
    • 戻り値 組み合わせが正しい場合 true それ以外 false

参考

Udemy Node.js + Express + MySQL で作る 安全な Webアプリケーション 実践

CSRF攻撃対策についてNode.js Expressでアプリを構築して実例で理解する

2022年3月25日 Node.js Express ルーティング

ルーティングとは

リクエストに対して処理の振り分けを行う仕組みの事です。

expressの基本機能です。

ルーティングの実装

app.(path, callback)

関数名

METHOD ⇨ リクエストメソッド(get, post, put, deleteなど)

path ⇨ 振り分けたいURL

callback ⇨ pathマッチした時の処理

GETメソッドで /home/indexにアクセスするには

const express = require("express");
const app = express();

app get("/home/index", (req, res, next) => {
  res.status(200).send("OK"); 
});

app.listen(3000);
  • ミドルウェアと似ていますがnext() がいらなく、responseに対して何かを返す必要があります。
  • sendメソッドでOKという文字列を返しています。

ルーティングのモジュール化

ある程度の機能単位でモジュール化する実装方法

express.Router()を使ってモジュール化します。

モジュール化した先のファイル

const router = require("express").Router();

router.get("/index", (req, res, next) => {
  res.status(200).send("OK");
});

module.exports = router;

読み込みファイル

const express = require("express");
const app = express();

app use("/index", require("./routes/index.js");

app.listen(3000);
  • useメソッドの第二引数でモジュールでエクスポートしたものを受け取っています。

パスパラメータ

サーバー側にデータを渡す方法です。

  • urlのパスの中に変数を埋め込みます。
  • http://localhost3000/shops/123だとしたら123がパスパラメータに当たります。(ID)
const express = require("express");
const app = express();

app.get("/shops/:id, (req, res, next) => {
  res.status(200).send(req.params.id);
});

app.listen(3000);
  • getメソッド第一引数でパスパラメータを<PARAM_NAME>で指定します。
  • 取得されたパラメータはreq.paramsから変数として取り出します。
  • 取り出す変数名は/shops/:id:パラメータ名 で取得可能です。

node expressで使えるHTMLのテンプレートエンジンについて

  • HTMLの雛形に対して動的に値やHTMLタグを埋め込む仕組みです。
  • HTMLの雛形に動的なデータを埋め込んでHTMLを返します。

ejs

種類は他にもあり記法が変わります。

私はRailsのerbをよく使っていたので記法はejsが直感的だったのでejsを使います。

<body>
  <h1><%= title %></h1>
</body>

こういった記法を用います。

基本構文

種類

値出力(エスケープ)

基本これを使います。XXS対策にもなります。

<%= %>

値出力(アンエスケープ)

インクルードの時のみ使用

<%- %>

コード実行

<% %>

コメント

<%# %>

インクルードとは

テンプレートの中にテンプレートを読み込ませる仕組みです。

<%- include(path [, locals]) %>

引数

  • path インクルードしたいテンプレートのファイルパス
  • locals インクルードするテンプレートに引き渡すデータ

戻り値

  • 生成されたHTML

参考

Udemy Node.js + Express + MySQL で作る 安全な Webアプリケーション 実践

2022年3月25日 Node.js Express

ストリームインスタンス

Node.jsによって提供される多くのストリームオブジェクトがあります。

例えば、HTTPサーバーへのリクエストとprocess.stdoutはどちらもストリームインスタンスです。

pipeイベント

読み取り可能なストリームでstream.pipe()メソッドが呼び出されたときに発行され、この書き込み可能なオブジェクトを宛先のセットに追加します。

const writer = getWritableStreamSomehow();
const reader = getReadableStreamSomehow();
writer.on('pipe', (src) => {
  console.log('Something is piping into the writer.');
  assert.equal(src, reader);
});
reader.pipe(writer);

process.stdoutプロパティ

stdoutに接続されたストリームを返します。fd 1がファイルを参照している場合を除き、net Socket(デュプレックスストリーム)です。ファイルを参照している場合は、書き込み可能なストリームです。

例として、process.stdiinをprocess.stdoutにコピーするには以下のようになります。

process.stdin.pipe(process.stdout);

process.env

Node.jsにおける環境変数はprocess.envというオブジェクトに格納されます。

このオブジェクトは最初から存在しており、環境変数が入っています。

ターミナルでnodeを入力しprocess.envを入力すると中身が見れます。

expressの可能不可能

できる事

  • リクエストのルーティング
  • レスポンスの基本的な整形

できない事

  • クッキー
  • セッション
  • 認証認可
  • DB接続

※できない事はミドルウェアを導入して利用できるようにします。

ミドルウェアとは

リクエストレスポンスに対して任意の追加処理を行う関数です。

  • ベースはexpressが処理をして足りないものをミドルウェアが補います。

できる事

  • リクエストオブジェクトの変更
  • レスポンスオブジェクトの変更
  • リクエスト/レスポンスを用いた独自の追加処理

等です。

ミドルウェアの実装

通常の処理

function (req, res, next) {
  // 処理
  next();
}
  • 引数は三つです。
  • 次のミドルウェアnext(); をつけて渡すようにする(しないと返ってこない処理になります)

エラー処理

function (err, req, res, next) {
  // 処理
  next();
}
  • 第一引数にエラーが含まれるので引数は4つです。

ミドルウェアの組み込み

const express require("express");
const app = express();

app.use((req, res, next) => {
});

app.listen(3000);
  • app.use()の引き渡す事で組み込めます。

利用時の注意点

  • 全てのリクエストに対して処理されます。
  • コードの上から順に実行されるので記述箇所に注意する。

参考

Udemy Node.js + Express + MySQL で作る 安全な Webアプリケーション 実践

2022年3月24日 Node.js入門 非同期ファイル読み書き

同期処理を行うとNode.jsの良さがなくなってしまうため利用は避けるべきです。

非同期処理の方法を見ていきます。

非同期ファイル読み込みをする場合

ファイルサイズが小さく後続処理も簡易な場合に利用します。

Promise 非同期処理の読み書き時にコールバックが多くなる問題の解消

util.promisifyメソッド

  • utilをrequireします。
  • util.promisifyメソッドを使用します。(Node.js v8以降の機能です)
  • util.promisifyメソッドはコールバックを渡すとPromise化されたオブジェクトが取得できます。
  • async awaitも使えるようになります。

util.promisifyメソッドを使った例

const fs = require("fs");
const path = require("path");
const util = require("util");

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

readFile(path.join(__dirname, "sample.txt"), "utf-8")
.then((data) => {
  return writeFile(path.join(__dirname, "sample.txt"), "utf-8")
})
.then(() => {
  console.log("OK");
})
.catch((error) => {
  console.log('error');
});

// 結果
OK
  • util.promisifyメソッドがPromiseのオブジェクトを返しているためthenメソッドでfulfilledの結果が受け取れています。
  • プロミスチェーンで最初のthenメソッドでfulfilled状態で返るため、二つ目のthenメソッドに処理が移っています。

async awaitで書き換えた例

const fs = require("fs");
const path = require("path");
const util = require("util");

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

(async function() {
  try {
    const data = await readFile(path.join(__dirname, "sample.txt"), "utf-8");
    await writeFile(path.join(__dirname, "sample.txt"), "utf-8");
    console.log(writeFile);
    console.log("OK");
  } catch(error) {
    console.log(error.message);
  }
})();

// 結果
OK
  • async functionを即時実行関数で定義しています。
  • async function直下でのawait式の挙動が得られるため非同期処理を同期処理のようにtry...catch構文を使用できています。

ストリーム

ストリームは大量のデータを小分けにして繰り返し処理を行います。一度に大量のデータを読み込まない事が特徴であり、メリットです。(リソースを一気に使ってしまわないようにできます)

※ファイルサイズが大きく後続処理が複雑な場合に利用します。

ストリームの種類

種類 説明 実装例
Readable Stream 読み込みができるストリーム fs.ReadStream
Writable Stream 書き込みができるストリーム fs.WriteStream
Duplex Stream 読み書き両方ができるストリーム net.Socket
Transform Stream 読み書きのタイミングでデータ加工ができるストリーム crypto.Chipher cryoto.Dechipher zlib.Gzip zlib.Gunzip

ReadableStream ファイル読み込み

イベント

  • data 小さなデータ一個読み込んだ時に発火
  • end 全てのデータが読み終わった時に発火

メソッド

  • pause() 読み込みを一時的に停止
  • resume() データ読み込み開始, 再開
  • pipe(stream) 次のストリームへデータを渡す

参考

Udemy Node.js入門)

2022年3月24日 Node.js入門 EventEmitter

Node.jsにおけるイベント機能の中心的存在

オブジェクトを持つイベントを管理する機能がまとめられている

  • イベントが発生したとき実行する処理をあらかじめ設定します。
  • イベントが発生したとき実行する処理を設定から削除します。
  • 実際にイベント発生させます。

基本機能

イベント発火時の処理を設定する

EventEmitter.on(name, listener)
EventEmitter.once(name, listener)

イベント発火時の処理を削除する

EventEmitter.off(name, listener)

イベントを発火する

EventEmitter.emit(name, args)
  • onceは一度きりの実行のイベントに使用します。
  • 削除する際は第二引数にコールバック関数を渡す必要があります。
  • 一度発火したイベントはoffメソッドで止めることはできません。ロジックで止める必要があります。

利用方法

EventEmitterを継承したクラスの利用

//インスタンス作成
const obj = new Child();

//イベントの設定
obj.on('tick', (message) => {
    console.log(message);
});

//イベントの発火
obj.emit('tick', "Hello world");
  • emitメソッドがイベント発生させます。
  • 第一引数のtickという同じイベント名が実行されるのでonメソッドのイベントが発生します。
  • 第二引数以降に渡された値がイベント処理側に渡されます。

実用例

clock.js

const EventEmitter = require("events");
const Clock = class extends EventEmitter {
  constructor() {
    super();
    this.interval = 2000;
    this.timer = null;
  }
  start() {
    if(this.timer) {
      this.stop();
    }
    this.timer = global.setInterval(() => {
      this.emit('tick');
    }, this.interval);
  }
  stop() {
    if(!this.timer) {
      return;
    }
    global.clearInterval(this.timer);
    this.timer = null;
  }
};
module.exports = Clock;

index.js

const Clock = require("./clock.js");
let i = 0;
const clock = new Clock();
clock.on('tick', () => {
  console.log(++i)
  if(i > 3) {
    clock.stop();
  }
});
clock.start();

処理結果

f:id:ryujisurf55:20220324162651p:plain

  • clock.jsでイベントエミッターを設定してエクスポートしています。
  • index.jsでrequire を行っています。

イベント発火時emit()実行時のポイント

  • emit()で呼び出される処理は同期呼び出しです。
  • on()に設定された順に呼び出します。
const EventEmitter = require("events");
const ee = new EventEmitter;

ee.on("event", () => { console.log("1st");});
ee.on("event", () => { console.log("2st");});

ee.emit("event");

//結果
1st
2st
  • EventEmitterをrequireインスタンス化しています。
  • onメソッドでイベントを登録して第一引数にイベント名を指定して第二引数にイベントの処理を記述しています。
  • emitメソッドの引数と同名のイベントが発火しています。
  • onメソッドで定義したイベントが順番に出力されています。

参考

Udemy Node.js入門

2022年3月24日 Node.js入門 イベントループ

イベントループ

処理実行順のことです。

フェーズとキュー

6つフェーズと2つのキューがあります。

フェーズ

  • Times
  • pending callback
  • idle
  • poll
  • check
  • close callback

 キュー

  • nextTickQueue
  • microTaskQueue

フェーズのみの場合はフェーズを順番に処理して回っていきます。

フェーズとキューがある場合はまずキューが実行されキューが終えたらフェーズが移行しまたキューに移行しにいくことを繰り返していきます。

キュー⇨フェーズ⇨キューの順番です。

処理内容

フェーズ 処理内容
Timers setTimeout(),setinterval()
pending callback I/O完了処理、I/O例外処理
idole Node.js内部処理
poll I/O処理
check setImmediate()
close callback I/O切断処理
キュー 処理内容
nextTickQueue nextTick()
microTaskQueue Promise

画像本教材から引用 f:id:ryujisurf55:20220324054850p:plain

参考

Udemy Node.js入門