Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add branch to implement freeCodeCamp tasks. #1

Merged
merged 15 commits into from
Feb 26, 2024
Merged

Add branch to implement freeCodeCamp tasks. #1

merged 15 commits into from
Feb 26, 2024

Conversation

akiko-pusu
Copy link
Owner

@akiko-pusu akiko-pusu commented Feb 25, 2024

This is a pull request to complete freeCodeCamp information-security project.
https://www.freecodecamp.org/learn/information-security

Information Security with HelmetJS

  1. Install and Require Helmet
  2. Hide Potentially Dangerous Information Using helmet.hidePoweredBy()
  3. Mitigate the Risk of Clickjacking with helmet.frameguard()
  4. Mitigate the Risk of Cross Site Scripting (XSS) Attacks with helmet.xssFilter()
  5. Avoid Inferring the Response MIME Type with helmet.noSniff()
  6. Prevent IE from Opening Untrusted HTML with helmet.ieNoOpen()
  7. Ask Browsers to Access Your Site via HTTPS Only with helmet.hsts()
  8. Disable DNS Prefetching with helmet.dnsPrefetchControl()
  9. Disable Client-Side Caching with helmet.noCache()
  10. Set a Content Security Policy with helmet.contentSecurityPolicy()
  11. Configure Helmet Using the ‘parent’ helmet() Middleware
  12. Understand BCrypt Hashes
  13. Hash and Compare Passwords Asynchronously
  14. Hash and Compare Passwords Synchronously

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 25, 2024

  1. Install and Require Helmet

https://www.freecodecamp.org/learn/information-security/information-security-with-helmetjs/hide-potentially-dangerous-information-using-helmet-hidepoweredby

Passed:helmet version 3.21.3 should be in package.json

テストの流れ:

  • フォームに GitPod で起動したアプリケーションの URL を入力
  • freeCodeCamp 側の画面で、submit すると以下の箇所でリクエストを送信
  • package.json の中身が返ってくるので、その結果をパースして対応するバージョンの helmet があるかをチェックしている。
 (getUserInput) =>
  $.get(getUserInput('url') + '/_api/package.json').then(
    (data) => {
      const packJson = JSON.parse(data);
      const helmet = packJson.dependencies.helmet;
      assert(helmet === '3.21.3' || helmet === '^3.21.3');
    },
    (xhr) => {
      throw new Error(xhr.responseText);
    }
  );

対応するコードはこれ。

https://github.com/freeCodeCamp/freeCodeCamp/blob/main/curriculum/challenges/arabic/09-information-security/information-security-with-helmetjs/install-and-require-helmet.md?#L25-L41

ほんとは package.json は隠しファイルにすべきなので、多分セキュリティの設定をしていく感じ?

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 25, 2024

  1. Hide Potentially Dangerous Information Using helmet.hidePoweredBy()

そのままだとヘッダに x-powered-by: Express が返ってくる。以下、curl -v -I での結果の抜粋。

< HTTP/2 200
HTTP/2 200
< accept-ranges: bytes
accept-ranges: bytes
< cache-control: public, max-age=0
cache-control: public, max-age=0
< content-type: text/html; charset=UTF-8
content-type: text/html; charset=UTF-8
< date: Sun, 25 Feb 2024 13:30:39 GMT
date: Sun, 25 Feb 2024 13:30:39 GMT
< etag: W/"314-18de0512da8"
etag: W/"314-18de0512da8"
< last-modified: Sun, 25 Feb 2024 12:50:33 GMT
last-modified: Sun, 25 Feb 2024 12:50:33 GMT
< x-powered-by: Express
x-powered-by: Express
< content-length: 788
content-length: 788

Use the helmet.hidePoweredBy() middleware to remove the X-Powered-By header.

とのことなので修正してみる。

参考

https://expressjs.com/ja/advanced/best-practice-security.html

Helmet を使用する
[Helmet](https://www.npmjs.com/package/helmet) は、HTTP ヘッダーを適切に設定することによって、いくつかの既知の Web の脆弱性からアプリケーションを保護します。

Helmet は、実際には、セキュリティー関連の HTTP ヘッダーを設定する 9 個の小さなミドルウェア関数の単なる集合です。

Ref.
https://github.com/helmetjs/helmet/tree/main/middlewares/x-powered-by

Hackers can exploit known vulnerabilities in Express/Node if they see that your site is powered by Express (or whichever framework you use). For example, X-Powered-By: Express is sent in every HTTP request coming from Express, by default. This won't provide much security benefit (https://github.com/expressjs/express/pull/2813#issuecomment-159270428), but might help a tiny bit. It will also improve performance by reducing the number of bytes sent.

const hidePoweredBy = require("hide-powered-by");
app.use(hidePoweredBy());
Note: if you're using Express, you don't need this middleware and can just do this:

app.disable("x-powered-by");
  • hidePowerdBy() でもよさそげ。

テスト内容

https://github.com/freeCodeCamp/freeCodeCamp/blob/main/curriculum/challenges/arabic/09-information-security/information-security-with-helmetjs/hide-potentially-dangerous-information-using-helmet.hidepoweredby.md

(getUserInput) =>
  $.get(getUserInput('url') + '/_api/app-info').then(
    (data) => {
      assert.include(data.appStack, 'hidePoweredBy');
      assert.notEqual(data.headers['x-powered-by'], 'Express');
    },
    (xhr) => {
      throw new Error(xhr.responseText);
    }
  );

レスポンスの内容

/_api/app-info の中身

{"headers":{},"appStack":["hidePoweredBy"]}

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 25, 2024

  1. Mitigate the Risk of Clickjacking with helmet.frameguard()

Clickjacking の防止のためにヘッダを修正します。設定がないと他のサイトから <iframe> タグで他のサイトのコンテンツ(ドメイン)に表示されてしまいます。フィッシングサイトに悪用されるリスクがあります。

X-Frame-Options ヘッダをつけて、ブラウザに iFrame で他のドメインからの取り込むができないように指定します。

Ref. https://www.npmjs.com/package/frameguard

const frameguard = require("frameguard");

// Don't allow me to be in ANY frames:
app.use(frameguard({ action: "deny" }));

// Only let me be framed by people of the same origin:
app.use(frameguard({ action: "sameorigin" }));
app.use(frameguard()); // defaults to sameorigin

If your app does not need to be framed (and most don't) you can use DENY. If your site can be in frames from the same origin, you can set it to SAMEORIGIN.

デフォルトは SAMEORIGIN になるかんじ。(normalize で大文字に変換)
https://github.com/helmetjs/helmet/blob/8d108f3cc9f9e2d454763ca59e7b2aa1409dbcd2/middlewares/x-frame-options/index.ts#L7C10-L9

function getHeaderValueFromOptions({
  action = "sameorigin",
}: Readonly<XFrameOptionsOptions>): string {
const normalizedAction =
    typeof action === "string" ? action.toUpperCase() : action;

freeCodeCamp がわのテストコードはここ。

https://github.com/freeCodeCamp/freeCodeCamp/blob/main/curriculum/challenges/arabic/09-information-security/information-security-with-helmetjs/mitigate-the-risk-of-clickjacking-with-helmet.frameguard.md

(getUserInput) =>
  $.get(getUserInput('url') + '/_api/app-info').then(
    (data) => {
      assert.include(
        data.appStack,
        'frameguard',
        'helmet.frameguard() middleware is not mounted correctly'
      );
    },
    (xhr) => {
      throw new Error(xhr.responseText);
    }
  );
helmet.frameguard() 'action' should be set to 'DENY'

(getUserInput) =>
  $.get(getUserInput('url') + '/_api/app-info').then(
    (data) => {
      assert.property(data.headers, 'x-frame-options');
      assert.equal(data.headers['x-frame-options'], 'DENY');
    },
    (xhr) => {
      throw new Error(xhr.responseText);
    }
  );

修正したあと、テスト用のレスポンスはこちら。

{"headers":{"x-frame-options":"DENY"},"appStack":["hidePoweredBy","frameguard"]}

feedback memo

一部日本語化が完了してないところがあるので、可能なら対応する!
https://www.freecodecamp.org/japanese/learn/information-security/information-security-with-helmetjs/mitigate-the-risk-of-clickjacking-with-helmet-frameguard

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 26, 2024

  1. Mitigate the Risk of Cross Site Scripting (XSS) Attacks with helmet.xssFilter()

https://github.com/helmetjs/helmet/blob/8d108f3cc9f9e2d454763ca59e7b2aa1409dbcd2/CHANGELOG.md?plain=1#L236

helmet.xssFilter` now disables the buggy XSS filter by default. See #230

どうやら disable になったみたい。ふむ。X-XSS-Protection を使ってね、になっている。
xXssProtection を使うほうがいいんだけれど、fcc のテストコードでは xXssProtection が期待値になっているので、xXssProtection でもよさそう。

https://github.com/freeCodeCamp/freeCodeCamp/blob/main/curriculum/challenges/arabic/09-information-security/information-security-with-helmetjs/mitigate-the-risk-of-cross-site-scripting-xss-attacks-with-helmet.xssfilter.md

(getUserInput) =>
  $.get(getUserInput('url') + '/_api/app-info').then(
    (data) => {
      assert.include(data.appStack, 'xXssProtection');
      assert.property(data.headers, 'x-xss-protection');
    },
    (xhr) => {
      throw new Error(xhr.responseText);
    }
  );

xXssProtection で設定した場合のヘッダとテストに使うレスポンスの出力結果。

< HTTP/1.1 200 OK
< X-Frame-Options: DENY
< X-XSS-Protection: 1; mode=block
< Access-Control-Allow-Origin: https://www.freecodecamp.org
< Access-Control-Allow-Headers: Origin, X-Requested-With, content-type, Accept
< Content-Type: application/json; charset=utf-8
< Content-Length: 132
< ETag: W/"84-pzTU52UoTXH71vtQaN+/6mR4ueY"
< Date: Mon, 26 Feb 2024 00:35:18 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
{"headers":{"x-frame-options":"DENY","x-xss-protection":"1; mode=block"},"appStack":["hidePoweredBy","frameguard","xXssProtection"]}

feedback 用の memo:

fcc側のドキュメントも、Use helmet.xssFilter() to sanitize input sent to your server. ってあるけど変えたほうがいいかもしれないですね!

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 26, 2024

  1. Avoid Inferring the Response MIME Type with helmet.noSniff()

Ref. MDN
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options

[Syntax](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options#syntax)

X-Content-Type-Options: nosniff

Blocks a request if the request destination is of type style and the MIME type is not text/css, or of type script and the MIME type is not a [JavaScript MIME type](https://html.spec.whatwg.org/multipage/scripting.html#javascript-mime-type).

ブラウザはファイルの拡張子から、コンテンツの mime type を判断しようとしたり、コンテンツの中身から mime type を判断しようとします。
JSON レスポンスが返るのに、text/html としてブラウザが扱うと、クロスサイトスクリプティングのような危険性があるので、明示的に X-Content-Type-Options: nosniff を指定して、ブラウザが勝手な判断を行わないようにします。
Content-Type ヘッダでの指定を遵守させます。

feedBack memo

一部日本語化が完了していない箇所があるので、対応してみる。
https://www.freecodecamp.org/japanese/learn/information-security/information-security-with-helmetjs/avoid-inferring-the-response-mime-type-with-helmet-nosniff

ソース変更前

localhost:3000/_api/app-info のリクエストは、Content-Type: application/json; charset=utf-8 になっている。でも、X-Content-Type-Options: nosniff の指定は無し。

< HTTP/1.1 200 OK
< X-Frame-Options: DENY
< X-XSS-Protection: 1; mode=block
< Access-Control-Allow-Origin: https://www.freecodecamp.org
< Access-Control-Allow-Headers: Origin, X-Requested-With, content-type, Accept
< Content-Type: application/json; charset=utf-8
< Content-Length: 132
< ETag: W/"84-pzTU52UoTXH71vtQaN+/6mR4ueY"
< Date: Mon, 26 Feb 2024 01:53:47 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
{"headers":{"x-frame-options":"DENY","x-xss-protection":"1; mode=block"},"appStack":["hidePoweredBy","frameguard","xXssProtection"]}

res.json({ headers: hObj, appStack: appMainRouteStack });

レスポンスを返す際に、res.json としているので。以下のようにすると、text/plain で返します。

res.type("txt").send({ headers: hObj, appStack: appMainRouteStack });

ソース修正

helmet の middleware 参照。
https://github.com/helmetjs/helmet/tree/main/middlewares/x-content-type-options

helmet.noSniff() ではなくて、app.use(dontSniffMimetype()) でやってみます。
変更後のレスポンス。X-Content-Type-Options: nosniff が設定。
レスポンスも appStack に nosniff のミドルウェアが返ります。

< HTTP/1.1 200 OK
< X-Frame-Options: DENY
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< Access-Control-Allow-Origin: https://www.freecodecamp.org
< Access-Control-Allow-Headers: Origin, X-Requested-With, content-type, Accept
< Content-Type: application/json; charset=utf-8
< Content-Length: 177
< ETag: W/"b1-pIZM4FBe4xK6Z/FZm+O7gqB4o4Q"
< Date: Mon, 26 Feb 2024 02:06:25 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
{"headers":{"x-frame-options":"DENY","x-xss-protection":"1; mode=block","x-content-type-options":"nosniff"},"appStack":["hidePoweredBy","frameguard","xXssProtection","nosniff"]}

テスト部分

https://github.com/freeCodeCamp/freeCodeCamp/blob/main/curriculum/challenges/arabic/09-information-security/information-security-with-helmetjs/avoid-inferring-the-response-mime-type-with-helmet.nosniff.md

(getUserInput) =>
  $.get(getUserInput('url') + '/_api/app-info').then(
    (data) => {
      assert.include(data.appStack, 'nosniff');
      assert.equal(data.headers['x-content-type-options'], 'nosniff');
    },
    (xhr) => {
      throw new Error(xhr.responseText);
    }
  );

このようにチェック。

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 26, 2024

  1. Prevent IE from Opening Untrusted HTML with helmet.ieNoOpen()

信頼できない HTML はオープンしないようにする。なるほど...。
あるバージョンのブラウザでは、信頼できない HTML をダウンロードしてオープンさせようとする。
X-Download-Options ヘッダを設定して、それを防ぎます。

Ref. X-Download-Options (Microsoft learn)
https://learn.microsoft.com/ja-jp/previous-versions/windows/internet-explorer/ie-developer/compatibility/jj542450(v=vs.85)#the-noopen-directive

The NoOpen Directive

When a user clicks Open on the download acceptance bar, Internet Explorer downloads the file to the temporary Internet files folder and doesn't keep a record of it in the download manager. If the user clicks Save, the file is saved to the Downloads folder and an entry is created in the download manager history.

To prevent users from opening a file immediately (and potentially losing track of the downloaded file) developers can specify the NoOpen directive. This way the user won’t accidentally open the file and lose it in the Temporary Internet Files folder.

You can set this directive in one of two ways:

The [X-Download-Options](https://go.microsoft.com/fwlink/p/?linkid=256567) HTTP response header (supported by Windows Internet Explorer 8 and later).
The download options meta tag <meta name="DownloadOptions" content="noopen"> (supported by Microsoft Internet Explorer 6 and later).

Ref.
https://learn.microsoft.com/en-us/archive/blogs/ie/ie8-security-part-v-comprehensive-protection

MIME-Handling: Force Save

Lastly, for web applications that need to serve untrusted HTML files, we have introduced a mechanism to help prevent the untrusted content from compromising your site’s security. When the new X-Download-Options header is present with the value noopen, the user is prevented from opening a file download directly; instead, they must first save the file locally. When the locally saved file is later opened, it no longer executes in the security context of your site, helping to prevent script injection.

HTTP/1.1 200 OK
Content-Length: 238
Content-Type: text/html
X-Download-Options: noopenContent-Disposition: attachment;
  • IE 限定になります。
  • もはや IE は使われてないと信じたいですが、Edge で IE 互換モードで動いてたり、業務アプリケーションで IE が使われている可能性があるので、このヘッダも設定しましょう。

Ref: Helmet / x-download-options
https://github.com/helmetjs/helmet/tree/main/middlewares/x-download-options

テスト部分

https://github.com/freeCodeCamp/freeCodeCamp/blob/main/curriculum/challenges/arabic/09-information-security/information-security-with-helmetjs/prevent-ie-from-opening-untrusted-html-with-helmet.ienoopen.md

(getUserInput) =>
  $.get(getUserInput('url') + '/_api/app-info').then(
    (data) => {
      assert.include(data.appStack, 'ienoopen');
      assert.equal(data.headers['x-download-options'], 'noopen');
    },
    (xhr) => {
      throw new Error(xhr.responseText);
    }
  );

feedbac kmemo

日本語化完了しています!
https://www.freecodecamp.org/japanese/learn/information-security/information-security-with-helmetjs/prevent-ie-from-opening-untrusted-html-with-helmet-ienoopen

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 26, 2024

  1. Ask Browsers to Access Your Site via HTTPS Only with helmet.hsts()

Ref. HSTS (MDN)
https://developer.mozilla.org/en-US/docs/Glossary/HSTS

HTTP Strict Transport Security lets a website inform the browser that it should never load the site using HTTP and should automatically convert all attempts to access the site using HTTP to HTTPS requests instead. It consists in one HTTP header, Strict-Transport-Security, sent by the server with the resource.

Ref. helmet - strict-transport-security
https://github.com/helmetjs/helmet/tree/main/middlewares/strict-transport-security

This middleware adds the Strict-Transport-Security header to the response. This tells browsers, "hey, only use HTTPS for the next period of time". (See the spec for more.) Note that the header won't tell users on HTTP to switch to HTTPS, it will just tell HTTPS users to stick around. You can enforce HTTPS with the express-enforces-ssl module.

Gitpod ではリバースプロキシで Strict-Transport-Security ヘッダが設定されているっぽい。
force オプションで値を上書きしてねとの説明あり。
わたしは localhost での確認です。

helmet.hsts() ではなく、ドキュメントに記載の方法で調整します。

const strictTransportSecurity = require("hsts");

// Sets "Strict-Transport-Security: max-age=15552000; includeSubDomains"
app.use(
  strictTransportSecurity({
    maxAge: 15552000, // 180 days in seconds
  }),
);

調整後

ヘッダはこのようになりました。

< HTTP/1.1 200 OK
< X-Frame-Options: DENY
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< X-Download-Options: noopen
< Strict-Transport-Security: max-age=7776000; includeSubDomains ← Here!
< Access-Control-Allow-Origin: https://www.freecodecamp.org
< Access-Control-Allow-Headers: Origin, X-Requested-With, content-type, Accept
< Content-Type: application/json; charset=utf-8
< Content-Length: 290
< ETag: W/"122-dpDM3sQf4pCpC0JQyiJOSQYIH0U"
< Date: Mon, 26 Feb 2024 04:14:55 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
{"headers":{"x-frame-options":"DENY","x-xss-protection":"1; mode=block","x-content-type-options":"nosniff","x-download-options":"noopen","strict-transport-security":"max-age=7776000; includeSubDomains"},"appStack":["hidePoweredBy","frameguard","xXssProtection","nosniff","ienoopen","hsts"]}

テスト部分

https://github.com/freeCodeCamp/freeCodeCamp/blob/main/curriculum/challenges/arabic/09-information-security/information-security-with-helmetjs/ask-browsers-to-access-your-site-via-https-only-with-helmet.hsts.md

(getUserInput) =>
  $.get(getUserInput('url') + '/_api/app-info').then(
    (data) => {
      assert.match(
        data.headers['strict-transport-security'],
        /^max-age=7776000;?/
      );
    },
    (xhr) => {
      throw new Error(xhr.responseText);
    }
  );

feedback memo

日本語化、一部英文が残っています。
https://www.freecodecamp.org/japanese/learn/information-security/information-security-with-helmetjs/ask-browsers-to-access-your-site-via-https-only-with-helmet-hsts

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 26, 2024

  1. Disable DNS Prefetching with helmet.dnsPrefetchControl()

パフォーマンス(レスポンス)向上のためにブラウザは DNS のプリフェッチの機能を備えている。
パフォーマンス的には嬉しいけれど、ユーザーがその次にどのサイト(ドメイン)にリクエストを送信するかの予測ができてしまい、プライバシー情報が漏れてしまう可能性がある。
ページ訪問数が水増しされてしまう可能性もある。
ユーザーを守ることと、パフォーマンスとのトレードオフで設定しましょう。

helmet の場合は helmet.dnsPrefetchControl() を使う。
X-DNS-Prefetch-Control を設定。

Ref.
https://github.com/helmetjs/helmet/tree/main/middlewares/x-dns-prefetch-control

This middleware lets you set the X-DNS-Prefetch-Control to control browsers' DNS prefetching. Read more about it on MDN and on Chromium's docs.

const dnsPrefetchControl = require("dns-prefetch-control");

// Set X-DNS-Prefetch-Control: off
app.use(dnsPrefetchControl());

// Set X-DNS-Prefetch-Control: off
app.use(dnsPrefetchControl({ allow: false }));

// Set X-DNS-Prefetch-Control: on
app.use(dnsPrefetchControl({ allow: true }));

調整後のヘッダ

< HTTP/1.1 200 OK
< X-Frame-Options: DENY
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< X-Download-Options: noopen
< Strict-Transport-Security: max-age=7776000; includeSubDomains
< X-DNS-Prefetch-Control: off
< Access-Control-Allow-Origin: https://www.freecodecamp.org
< Access-Control-Allow-Headers: Origin, X-Requested-With, content-type, Accept
< Content-Type: application/json; charset=utf-8
< Content-Length: 342
< ETag: W/"156-DfYQYyClGpuameok42G4EZbrBTA"
< Date: Mon, 26 Feb 2024 04:35:14 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
{"headers":{"x-frame-options":"DENY","x-xss-protection":"1; mode=block","x-content-type-options":"nosniff","x-download-options":"noopen","strict-transport-security":"max-age=7776000; includeSubDomains","x-dns-prefetch-control":"off"},"appStack":["hidePoweredBy","frameguard","xXssProtection","nosniff","ienoopen","hsts","dnsPrefetchControl"]}

テストコード

https://github.com/freeCodeCamp/freeCodeCamp/blob/main/curriculum/challenges/arabic/09-information-security/information-security-with-helmetjs/disable-dns-prefetching-with-helmet.dnsprefetchcontrol.md

(getUserInput) =>
  $.get(getUserInput('url') + '/_api/app-info').then(
    (data) => {
      assert.include(data.appStack, 'dnsPrefetchControl');
      assert.equal(data.headers['x-dns-prefetch-control'], 'off');
    },
    (xhr) => {
      throw new Error(xhr.responseText);
    }
  );

x-dns-prefetch-control ヘッダが Off であることをチェック。

feedback memo

ここは日本語化完了みたい。
https://www.freecodecamp.org/japanese/learn/information-security/information-security-with-helmetjs/disable-dns-prefetching-with-helmet-dnsprefetchcontrol

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 26, 2024

  1. Disable Client-Side Caching with helmet.noCache()

If you are releasing an update for your website, and you want the users to always download the newer version, you can (try to) disable caching on client’s browser. It can be useful in development too. Caching has performance benefits, which you will lose, so only use this option when there is a real need.

サイトの更新を行なった際は、クライアント側のキャッシュを無効化しましょう。
開発モードの時も。キャッシュによりパフォーマンスは向上しますが、無効化することによってパフォーマンスが失われますので、ここは必要に応じて。

Ref. Remove noCache from "mainline" Helmet
helmetjs/helmet#215

In an effort to focus this project's scope, I'm trying to make sure that Helmet only focuses on HTTP response headers that are strictly related to security. While nocache is useful, it's not strictly security-related in my eyes, so I'm removing it.

4.0 以後のリリースでは helment.noCache() は削除になったらしい。
そのかわり、別の npm を使ってね、とのこと。

freeCodeCamp での helment は "helmet": "3.21.3" の指定なのでコード上は使えそう。

package-lock.json で nocache とあるので、これでよさそう。

		"helmet": {
			"version": "3.21.3",
			"resolved": "https://registry.npmjs.org/helmet/-/helmet-3.21.3.tgz",
			"requires": {
				"depd": "2.0.0",
				"dns-prefetch-control": "0.2.0",
				"dont-sniff-mimetype": "1.1.0",
				"expect-ct": "0.2.0",
				"feature-policy": "0.3.0",
				"frameguard": "3.1.0",
				"helmet-crossdomain": "0.4.0",
				"helmet-csp": "2.9.5",
				"hide-powered-by": "1.1.0",
				"hpkp": "2.0.0",
				"hsts": "2.2.0",
				"ienoopen": "1.1.0",
				"nocache": "2.1.0",
				"referrer-policy": "1.2.0",
				"x-xss-protection": "1.3.0"
			}
		}

調整後のヘッダ

< HTTP/1.1 200 OK
< X-Frame-Options: DENY
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< X-Download-Options: noopen
< Strict-Transport-Security: max-age=7776000; includeSubDomains
< X-DNS-Prefetch-Control: off
< Surrogate-Control: no-store
< Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate
< Pragma: no-cache
< Expires: 0
< Access-Control-Allow-Origin: https://www.freecodecamp.org
< Access-Control-Allow-Headers: Origin, X-Requested-With, content-type, Accept
< Content-Type: application/json; charset=utf-8
< Content-Length: 489
< ETag: W/"1e9-cewx+jzqUQqMWvjX8NckmLPLc0I"
< Date: Mon, 26 Feb 2024 06:57:52 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
{"headers":{"x-frame-options":"DENY","x-xss-protection":"1; mode=block","x-content-type-options":"nosniff","x-download-options":"noopen","strict-transport-security":"max-age=7776000; includeSubDomains","x-dns-prefetch-control":"off","surrogate-control":"no-store","cache-control":"no-store, no-cache, must-revalidate, proxy-revalidate","pragma":"no-cache","expires":"0"},"appStack":["hidePoweredBy","frameguard","xXssProtection","nosniff","ienoopen","hsts","dnsPrefetchControl","nocache"]}

feedback memo

日本語化済み。
https://www.freecodecamp.org/japanese/learn/information-security/information-security-with-helmetjs/disable-client-side-caching-with-helmet-nocache

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 26, 2024

  1. Set a Content Security Policy with helmet.contentSecurityPolicy()

CSP の設定。helmet-csp で良さそう?
Ref. https://github.com/helmetjs/helmet/blob/main/middlewares/content-security-policy/README.md

const contentSecurityPolicy = require("helmet-csp");

app.use(
  contentSecurityPolicy({
    useDefaults: true,
    directives: {
      defaultSrc: ["'self'", "default.example"],
      scriptSrc: ["'self'", "js.example.com"],
      objectSrc: ["'none'"],
      upgradeInsecureRequests: [],
    },
    reportOnly: false,
  }),
);

Ref. コンテンツセキュリティポリシー (CSP) ftom MDN
https://developer.mozilla.org/ja/docs/Web/HTTP/CSP

CSP を有効にするには、ウェブサーバーから Content-Security-Policy HTTP ヘッダーを返すように設定する必要があります(X-Content-Security-Policy ヘッダーに関する記述が時々ありますが、これは古いバージョンのものであり、今日このヘッダーを指定する必要はありません)。

設定後のヘッダ

< HTTP/1.1 200 OK
< X-Frame-Options: DENY
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< X-Download-Options: noopen
< Strict-Transport-Security: max-age=7776000; includeSubDomains
< X-DNS-Prefetch-Control: off
< Surrogate-Control: no-store
< Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate
< Pragma: no-cache
< Expires: 0
< Content-Security-Policy: default-src 'self'; script-src 'self' trusted-cdn.com
< X-Content-Security-Policy: default-src 'self'; script-src 'self' trusted-cdn.com
< X-WebKit-CSP: default-src 'self'; script-src 'self' trusted-cdn.com
< Access-Control-Allow-Origin: https://www.freecodecamp.org
< Access-Control-Allow-Headers: Origin, X-Requested-With, content-type, Accept
< Content-Type: application/json; charset=utf-8
< Content-Length: 732
< ETag: W/"2dc-vpoJVCDcERk/AblZS0gR4rl/kAg"
< Date: Mon, 26 Feb 2024 07:40:57 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
{"headers":{"x-frame-options":"DENY","x-xss-protection":"1; mode=block","x-content-type-options":"nosniff","x-download-options":"noopen","strict-transport-security":"max-age=7776000; includeSubDomains","x-dns-prefetch-control":"off","surrogate-control":"no-store","cache-control":"no-store, no-cache, must-revalidate, proxy-revalidate","pragma":"no-cache","expires":"0","content-security-policy":"default-src 'self'; script-src 'self' trusted-cdn.com","x-content-security-policy":"default-src 'self'; script-src 'self' trusted-cdn.com","x-webkit-csp":"default-src 'self'; script-src 'self' trusted-cdn.com"},"appStack":["hidePoweredBy","frameguard","xXssProtection","nosniff","ienoopen","hsts","dnsPrefetchControl","nocache","csp"]}

ヘッダが増えました..。

テスト部分

https://github.com/freeCodeCamp/freeCodeCamp/blob/main/curriculum/challenges/arabic/09-information-security/information-security-with-helmetjs/set-a-content-security-policy-with-helmet.contentsecuritypolicy.md

(getUserInput) =>
  $.get(getUserInput('url') + '/_api/app-info').then(
    (data) => {
      var cspHeader = Object.keys(data.headers).filter(function (k) {
        return (
          k === 'content-security-policy' ||
          k === 'x-webkit-csp' ||
          k === 'x-content-security-policy'
        );
      })[0];
      assert.equal(
        data.headers[cspHeader],
        "default-src 'self'; script-src 'self' trusted-cdn.com"
      );
    },
    (xhr) => {
      throw new Error(xhr.responseText);
    }
  );

feedback memo

少しだけ英文が残っています。
https://www.freecodecamp.org/japanese/learn/information-security/information-security-with-helmetjs/set-a-content-security-policy-with-helmet-contentsecuritypolicy

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 26, 2024

  1. Configure Helmet Using the ‘parent’ helmet() Middleware

app.use(helmet()) will automatically include all the middleware introduced above, except noCache(), and contentSecurityPolicy(), but these can be enabled if necessary. You can also disable or configure any other middleware individually, using a configuration object.

app.use(helmet()) では、noCache() と contentSecurityPolicy() 以外のミドルウェアを自動的に組み込む。
ミドルウェアを disable にしたい場合は、明示的に設定しましょう、とのこと。

app.use(helmet({
  frameguard: {         // configure
    action: 'deny'
  },
  contentSecurityPolicy: {    // enable and configure
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ['style.com'],
    }
  },
  dnsPrefetchControl: false     // disable
}))

ここは特にテストはなくてもパス。(URLのみ)

feedback memo

日本語化は完了しています。
https://www.freecodecamp.org/japanese/learn/information-security/information-security-with-helmetjs/configure-helmet-using-the-parent-helmet-middleware

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 26, 2024

  1. Understand BCrypt Hashes

ここは helmet ではなく bcrypt を追加が必要らしい。

テスト部分

https://github.com/freeCodeCamp/freeCodeCamp/blob/main/curriculum/challenges/arabic/09-information-security/information-security-with-helmetjs/understand-bcrypt-hashes.md

(getUserInput) =>
  $.get(getUserInput('url') + '/_api/package.json').then(
    (data) => {
      var packJson = JSON.parse(data);
      assert.property(
        packJson.dependencies,
        'bcrypt',
        'Your project should list "bcrypt" as a dependency'
      );
    },
    (xhr) => {
      throw new Error(xhr.statusText);
    }
  );
  • /_api/package.json のパスにリクエスト送信。
  • package.json の中身をチェックするらしい。
(getUserInput) =>
  $.get(getUserInput('url') + '/_api/server.js').then(
    (data) => {
      assert.match(
        data,
        /bcrypt.*=.*require.*('|")bcrypt('|")/gi,
        'You should correctly require and instantiate socket.io as io.'
      );
    },
    (xhr) => {
      throw new Error(xhr.statusText);
    }
  );
  • あとは server.js の中身もチェックするっぽい。
  • でも実装されてなさそうだ...。
  • 妥当なのかわからないけれど、テストコードを満たすように以下実装。
    • /_api/server.js のリクエストを受けてレスポンスを返す部分を追加
    • コンテンツにパターンマッチを満たす内容を追加。

設定結果

// server.js から抜粋
// /_api/package.json へのリクエストで、ファイルを読み込んで返しています
app.get("/package.json", function (req, res, next) {
  fs.readFile(__dirname + "/package.json", function (err, data) {
    if (err) return next(err);
    res.type("txt").send(data.toString());
  });
});
< Keep-Alive: timeout=5
<
{
	"name": "fcc-infosec-challenges",
	"version": "0.0.1",
	"description": "fcc backend boilerplate",
	"main": "server.js",
	"scripts": {
		"start": "node myApp.js"
	},
	"dependencies": {
		"bcrypt": "^5.1.1",
		"express": "^4.14.0",
		"helmet": "3.21.3"
	},
	"keywords": [
		"node",
		"hyperdev",
		"express",
		"freecodecamp"
	],
	"license": "MIT"
}
* Connection #0 to host localhost left intact

feedback memo

日本語対応済み!
https://www.freecodecamp.org/japanese/learn/information-security/information-security-with-helmetjs/understand-bcrypt-hashes

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 26, 2024

  1. Hash and Compare Passwords Asynchronously

非同期でパスワードをハッシュ化し、比較を行う。

As hashing is designed to be computationally intensive, it is recommended to do so asynchronously on your server as to avoid blocking incoming connections while you hash.

ハッシュ化は非常に高い計算処理を要するようにデザインされているので、サーバー側では非同期に処理をすることをお勧めします。ハッシュ化している最中に接続が途切れないようにするためです。

ハッシュの生成:

bcrypt.hash(myPlaintextPassword, saltRounds, (err, hash) => {
  /*Store hash in your db*/
});

ハッシュの比較:

bcrypt.compare(myPlaintextPassword, hash, (err, res) => {
  /*res == true or false*/
});

Ref. bcrypt
https://www.npmjs.com/package/bcrypt

bcrypt の非同期処理に関して。

bcrypt uses whatever Promise implementation is available in global.Promise. NodeJS >= 0.12 has a native Promise implementation built in. However, this should work in any Promises/A+ compliant implementation.
Async methods that accept a callback, return a Promise when callback is not specified if Promise support is available.

async/await が使えそう。

テストコード / 期待値

Async hash should be generated and correctly compared.

コンソールに出るのかなあ?
どんなリクエストが出るのかわかりにくいので、このコースはテストコードがわからないと詰みそう...。

https://github.com/freeCodeCamp/freeCodeCamp/blob/main/curriculum/challenges/arabic/09-information-security/information-security-with-helmetjs/hash-and-compare-passwords-asynchronously.md

(getUserInput) =>
  $.get(getUserInput('url') + '/_api/server.js').then(
    (data) => {
      assert.match(
        data,
        /START_ASYNC[^]*bcrypt.hash.*myPlaintextPassword( |),( |)saltRounds( |),( |).*err( |),( |)hash[^]*END_ASYNC/gi,
        'You should call bcrypt.hash on myPlaintextPassword and saltRounds and handle err and hash as a result in the callback'
      );
      assert.match(
        data,
        /START_ASYNC[^]*bcrypt.hash[^]*bcrypt.compare.*myPlaintextPassword( |),( |)hash( |),( |).*err( |),( |)res[^]*}[^]*}[^]*END_ASYNC/gi,
        'Nested within the hash function should be the compare function comparing myPlaintextPassword to hash'
      );
    },
    (xhr) => {
      throw new Error(xhr.statusText);
    }
  );
  • /_api/server.js にリクエストを送信するらしい。
  • START_ASYNC と END_ASYNC はパターンマッチに入っているけれど、定数じゃなくてコメント文でいいみたい。

server.js に差し込んで、というのはこのコード。ただし、テストコードのパターンマッチに合うように修正が必要。

bcrypt.hash('passw0rd!', 13, (err, hash) => {
  console.log(hash);
  //$2a$12$Y.PHPE15wR25qrrtgGkiYe2sXo98cjuMCG1YwSI5rJW1DSJp0gEYS
  bcrypt.compare('passw0rd!', hash, (err, res) => {
    console.log(res); //true
  });
});

feedback memo

一部英語の文章が残っています。
https://www.freecodecamp.org/japanese/learn/information-security/information-security-with-helmetjs/hash-and-compare-passwords-asynchronously

@akiko-pusu
Copy link
Owner Author

akiko-pusu commented Feb 26, 2024

  1. Hash and Compare Passwords Synchronously

Hashing synchronously is just as easy to do but can cause lag if using it server side with a high cost or with hashing done very often.
同期的なハッシュ化は簡単だけど、サーバーサイドでの処理にはコストがかかり、遅延も発生します。

たしかに、13 の実装だと、レスポンスを返したあとにコンソールに hash & compare の結果が出力された。
でも、ブラウザのコンソールにはエラーが出てしまっている。
bcrypt.hashSyncbcrypt.compareSync を使ってね、とのこと。

Ref.
https://www.npmjs.com/package/bcrypt#with-promises

const salt = bcrypt.genSaltSync(saltRounds);
const hash = bcrypt.hashSync(myPlaintextPassword, salt);

テストコード

https://github.com/freeCodeCamp/freeCodeCamp/blob/main/curriculum/challenges/arabic/09-information-security/information-security-with-helmetjs/hash-and-compare-passwords-synchronously.md

(getUserInput) =>
  $.get(getUserInput('url') + '/_api/server.js').then(
    (data) => {
      assert.match(
        data,
        /START_SYNC[^]*hash.*=.*bcrypt.hashSync.*myPlaintextPassword( |),( |)saltRounds[^]*END_SYNC/gi,
        'You should call bcrypt.hashSync on myPlaintextPassword with saltRounds'
      );
      assert.match(
        data,
        /START_SYNC[^]*result.*=.*bcrypt.compareSync.*myPlaintextPassword( |),( |)hash[^]*END_SYNC/gi,
        'You should call bcrypt.compareSync on myPlaintextPassword with the hash generated in the last line'
      );
    },
    (xhr) => {
      throw new Error(xhr.statusText);
    }
  );

compareSync や hashSync に置き換えればよさそう。
テストをパスするためには compareSync や hashSync を使うけれど、await await bcrypt.hash でもよさそげ。

app.get("/server.js", async function (req, res, next) {

  const myPlaintextPassword = 'passw0rd!';
  const saltRounds = 13;
  /*
  //START_ASYNC
  const hash = bcrypt.hashSync(myPlaintextPassword, saltRounds);
  console.log(hash);
  const result = bcrypt.compareSync(myPlaintextPassword, hash);
  console.log(result); //true
  // END_ASYNC
  */

  const hash = await bcrypt.hash(myPlaintextPassword, saltRounds);
  console.log(hash);

  const result = await bcrypt.compare(myPlaintextPassword, hash);
  console.log(result);
  // 略....

調整すると、ちゃんと result が取得されて Node.js 側のコンソールに出力されてからレスポンスを返すようになっています。

feedback memo

日本語化は完了しているっぽい。
https://www.freecodecamp.org/japanese/learn/information-security/information-security-with-helmetjs/hash-and-compare-passwords-synchronously

@akiko-pusu akiko-pusu merged commit 116f2d1 into main Feb 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
1 participant