-
Notifications
You must be signed in to change notification settings - Fork 0
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
Conversation
Passed:helmet version 3.21.3 should be in package.json テストの流れ:
(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);
}
); 対応するコードはこれ。 ほんとは package.json は隠しファイルにすべきなので、多分セキュリティの設定をしていく感じ? |
そのままだとヘッダに < 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
とのことなので修正してみる。 参考https://expressjs.com/ja/advanced/best-practice-security.html
Ref.
テスト内容(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"]} |
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
デフォルトは SAMEORIGIN になるかんじ。(normalize で大文字に変換) function getHeaderValueFromOptions({
action = "sameorigin",
}: Readonly<XFrameOptionsOptions>): string {
const normalizedAction =
typeof action === "string" ? action.toUpperCase() : action; freeCodeCamp がわのテストコードはここ。 (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);
}
); 修正したあと、テスト用のレスポンスはこちら。
feedback memo一部日本語化が完了してないところがあるので、可能なら対応する! |
どうやら disable になったみたい。ふむ。X-XSS-Protection を使ってね、になっている。 (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側のドキュメントも、 |
Ref. MDN
ブラウザはファイルの拡張子から、コンテンツの mime type を判断しようとしたり、コンテンツの中身から mime type を判断しようとします。 feedBack memo一部日本語化が完了していない箇所があるので、対応してみる。 ソース変更前localhost:3000/_api/app-info のリクエストは、
レスポンスを返す際に、res.json としているので。以下のようにすると、
ソース修正helmet の middleware 参照。
< 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"]} テスト部分(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);
}
); このようにチェック。 |
信頼できない HTML はオープンしないようにする。なるほど...。 Ref. X-Download-Options (Microsoft learn)
Ref.
Ref: Helmet / x-download-options テスト部分(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 |
Ref. HSTS (MDN)
Ref. helmet - strict-transport-security
Gitpod ではリバースプロキシで Strict-Transport-Security ヘッダが設定されているっぽい。 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"]} テスト部分(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 |
パフォーマンス(レスポンス)向上のためにブラウザは DNS のプリフェッチの機能を備えている。 helmet の場合は Ref.
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"]} テストコード(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 |
サイトの更新を行なった際は、クライアント側のキャッシュを無効化しましょう。 Ref. Remove noCache from "mainline" Helmet
4.0 以後のリリースでは helment.noCache() は削除になったらしい。 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 |
CSP の設定。helmet-csp で良さそう? 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
設定後のヘッダ< 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"]} ヘッダが増えました..。 テスト部分(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 |
app.use(helmet()) では、noCache() と contentSecurityPolicy() 以外のミドルウェアを自動的に組み込む。
ここは特にテストはなくてもパス。(URLのみ) feedback memo |
ここは helmet ではなく bcrypt を追加が必要らしい。 テスト部分(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);
}
);
(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/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 |
非同期でパスワードをハッシュ化し、比較を行う。
ハッシュ化は非常に高い計算処理を要するようにデザインされているので、サーバー側では非同期に処理をすることをお勧めします。ハッシュ化している最中に接続が途切れないようにするためです。 ハッシュの生成: bcrypt.hash(myPlaintextPassword, saltRounds, (err, hash) => {
/*Store hash in your db*/
}); ハッシュの比較: bcrypt.compare(myPlaintextPassword, hash, (err, res) => {
/*res == true or false*/
}); Ref. bcrypt bcrypt の非同期処理に関して。
テストコード / 期待値
コンソールに出るのかなあ? (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);
}
);
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一部英語の文章が残っています。 |
たしかに、13 の実装だと、レスポンスを返したあとにコンソールに hash & compare の結果が出力された。 Ref. const salt = bcrypt.genSaltSync(saltRounds);
const hash = bcrypt.hashSync(myPlaintextPassword, salt); テストコード(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 に置き換えればよさそう。 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日本語化は完了しているっぽい。 |
This is a pull request to complete freeCodeCamp information-security project.
https://www.freecodecamp.org/learn/information-security
Information Security with HelmetJS