上一篇提到基於 Base64 的身份驗證,以及寫死帳號密碼於程式碼是非常不安全的事情。
在這個地方提到資料庫相關的概念,針對資料庫,我們將設計幾個問題讓大家更認識資料庫是什麼。
問題: 當資料量變得龐大且複雜時,例如公司的人事資料、電商平台的商品資訊,單純使用筆記本或檔案記錄就會變得很困難。這時就需要用到「資料庫」來儲存和管理這些資訊。請問你們認為資料庫的出現解決了什麼問題?
延伸問題: 試著想像如果沒有資料庫,我們會面臨哪些困難?
「在實際的網站或應用程式中,我們應該如何安全地儲存和管理使用者資訊?」
https://github.com/fei3363/ithelp_web_security_2024/commit/5603fc4a2790e8342f5b96d8337b1b5af86ebe54
version: '3'
services:
app:
build:
context: ./web
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- MONGODB_URI=mongodb://mongo:27017/myapp
- POSTGRES_URI=postgresql://postgres:postgres@postgres:5432/myapp
- NODE_ENV=development
depends_on:
- mongo
- postgres
volumes:
- ./web:/usr/src/app
- /usr/src/app/node_modules
command: sh -c "npm install && npm start"
mongo:
image: mongo:latest
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
postgres:
image: postgres:latest
ports:
- "5432:5432"
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgresPassword
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
mongodb_data:
postgres_data:
MONGO_USER=your_mongo_user
MONGO_PASSWORD=your_mongo_password
POSTGRES_USER=your_postgres_user
POSTGRES_PASSWORD=your_postgres_password
environment:
version: '3'
services:
app:
build:
context: ./web
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- MONGODB_URI=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/myapp
- POSTGRES_URI=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/myapp
- NODE_ENV=development
depends_on:
- mongo
- postgres
volumes:
- ./web:/usr/src/app
- /usr/src/app/node_modules
command: sh -c "npm install && npm start"
mongo:
image: mongo:latest
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
volumes:
- mongodb_data:/data/db
postgres:
image: postgres:latest
ports:
- "5432:5432"
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
mongodb_data:
postgres_data:
啟動的時候導入 .env 檔案docker-compose --env-file .env up
Node.js 使用這些環境變量來連接資料庫
const mongoUri = process.env.MONGODB_URI;
const postgresUri = process.env.POSTGRES_URI;
我們需要設定與 PostgreSQL 資料庫的連接。
在 web/db/postgres.js
檔案中:
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.POSTGRES_URI,
});
module.exports = {
query: (text, params) => pool.query(text, params),
};
建立一個資料庫連接方法,並使用 exports 一個 query
函數來執行 SQL 查詢。
在 web/controllers/userController.js
文件中,我們定義了處理使用者相關操作的函數:
const db = require('../db/postgres');
const userController = {
// 建立使用者
async createUser(req, res) {
const { username, email, password } = req.body;
try {
const result = await db.query(
'INSERT INTO users(username, email, password) VALUES($1, $2, $3) RETURNING *',
[username, email, password]
);
res.status(201).json(result.rows[0]);
} catch (error) {
res.status(500).json({ error: error.message });
}
},
// 取得所有使用者
async getAllUsers(req, res) {
try {
const result = await db.query('SELECT * FROM users');
res.json(result.rows);
} catch (error) {
res.status(500).json({ error: error.message });
}
},
// 根據 ID 取得使用者
async getUserById(req, res) {
const { id } = req.params;
try {
const result = await db.query('SELECT * FROM users WHERE id = $1', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ message: '使用者未找到' });
}
res.json(result.rows[0]);
} catch (error) {
res.status(500).json({ error: error.message });
}
},
// 更新使用者
async updateUser(req, res) {
const { id } = req.params;
const { username, email } = req.body;
try {
const result = await db.query(
'UPDATE users SET username = $1, email = $2 WHERE id = $3 RETURNING *',
[username, email, id]
);
if (result.rows.length === 0) {
return res.status(404).json({ message: '使用者未找到' });
}
res.json(result.rows[0]);
} catch (error) {
res.status(500).json({ error: error.message });
}
},
// 刪除使用者
async deleteUser(req, res) {
const { id } = req.params;
try {
const result = await db.query('DELETE FROM users WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ message: '使用者未找到' });
}
res.json({ message: '使用者已成功刪除', deletedUser: result.rows[0] });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
};
module.exports = userController;
這個控制器包含了建立、讀取、更新和刪除使用者的功能。每個函數都使用 db.query
來執行相應的 SQL 操作。
在 web/routes/userRoutes.js
檔案,定義了 API 路由:
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
router.post('/', userController.createUser);
router.get('/', userController.getAllUsers);
router.get('/:id', userController.getUserById);
router.put('/:id', userController.updateUser);
router.delete('/:id', userController.deleteUser);
module.exports = router;
這些路由將 HTTP 請求對應到相應的控制器函數。
最後,在 web/server.js
文件中,將使用者路由整合到主 Express 應用程式:
const express = require('express');
const path = require('path');
const userRoutes = require('./routes/userRoutes');
const app = express();
// ... 其他設定 ...
app.use('/api/users', userRoutes);
// ... 啟動伺服器 ...
這個步驟代表,就將 /api/users
路徑下的請求導向到使用者路由處理器。
通過以上步驟,實現了一個基本的使用者管理系統,包括:
進入 Docker 容器:
root@localhost:~/nodejs_lab# docker exec -it nodejs_lab_postgres_1 bash
這個指令讓你從本機(localhost)進入名為「nodejs_lab_postgres_1」的 Docker 容器的 bash 殼層。
連線到 PostgreSQL:
root@317c8cf1536e:/# psql -U postgres
在容器內,使用 postgres 使用者身份連線到 PostgreSQL。這裡顯示了容器的 ID(317c8cf1536e)。
顯示 PostgreSQL 版本資訊:
psql (14.2 (Debian 14.2-1.pgdg110+1))
Type "help" for help.
這裡告訴我們 PostgreSQL 的版本是 14.2,運行在 Debian 系統上。
列出現有的資料庫:
postgres=> \l
這個指令列出所有現存的資料庫。從結果可以看到:
建立新資料庫:
postgres=> CREATE DATABASE myapp;
CREATE DATABASE
建立一個名為「myapp」的新資料庫。系統回應「CREATE DATABASE」表示成功了。
連線到新資料庫:
postgres=> \c myapp
You are now connected to database "myapp" as user "postgres".
切換到剛剛建立的「myapp」資料庫。系統確認你已經連線到新資料庫。
建立使用者資料表:
myapp=> CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE
在「myapp」資料庫中建立一個名為「users」的資料表。這個表格包含:
系統回應「CREATE TABLE」表示表格建立成功。
出現空白代表成功
curl -X POST http://nodelab.feifei.tw/api/users -H "Content-Type: application/json" -d '{"username": "testuser", "email": "testuser@example.com", "password": "password123"}'
curl http://nodelab.feifei.tw/api/users
資安問題:
關於關聯式資料庫和非關聯式資料庫的比較,下列哪項陳述是正確的?
A)關聯式資料庫更適合處理非結構化資料
B)非關聯式資料庫通常具有更好的資料一致性
C)關聯式資料庫支援複雜的查詢操作
D)非關聯式資料庫不適合大規模資料儲存
答案:C
解析:關聯式資料庫的一個主要優勢是支援複雜的查詢操作,特別是透過 SQL 語言。非關聯式資料庫通常更適合處理非結構化資料和大規模資料儲存,而關聯式資料庫則更擅長保證資料的一致性和支援複雜查詢。
在實作使用者認證系統時,下列哪種做法是最不安全的?
A)使用 HTTPS 協定傳輸資料
B)在資料庫中儲存密碼的雜湊值
C)使用 Base64 編碼儲存密碼
D)使用加鹽(salt)的密碼雜湊
答案:C
解析:使用 Base64 編碼儲存密碼是非常不安全的做法,因為 Base64 是一種可逆的編碼方式,很容易被解碼。正確的做法是使用不可逆的加密方法(如加鹽的密碼雜湊)來儲存密碼,同時使用 HTTPS 協定來保護資料傳輸。
在設定 PostgreSQL 資料庫連線時,以下哪種做法最安全?
A)在程式碼中直接寫入資料庫使用者名稱和密碼
B)使用環境變數儲存資料庫連線資訊
C)將連線資訊儲存在公開的設定檔中
D)使用固定的預設密碼方便團隊成員使用
答案:B
解析:使用環境變數儲存敏感資訊(如資料庫連線詳情)是一種良好的安全實務。這樣可以避免將敏感資訊直接寫入程式碼或設定檔中,減少資訊洩漏的風險。同時,它也便於在不同的環境(如開發、測試、生產)中使用不同的設定。
關於 SQL 注入攻擊,下列哪項陳述是正確的?
A)只有 MySQL 資料庫容易受到 SQL 注入攻擊
B)使用參數化查詢可以有效防止 SQL 注入
C)SQL 注入只能用於查詢資料,不能修改資料庫
D)對使用者輸入進行 Base64 編碼可以完全防止 SQL 注入
答案:B
解析:使用參數化查詢(也稱作預處理陳述式)是防止 SQL 注入攻擊的有效方法。它將 SQL 陳述式和資料分開,使得攻擊者無法插入惡意的 SQL 程式碼。SQL 注入攻擊可以影響所有類型的 SQL 資料庫,不僅限於查詢操作,還可能導致資料被修改或刪除。簡單的編碼(如 Base64)不足以防止 SQL 注入。
在 Node.js 應用程式中使用 Docker Compose 設定多個服務時,以下哪種做法是正確的?
A)所有服務都應該使用 root 使用者執行以確保足夠的權限
B)資料庫的使用者名稱和密碼應該直接寫在 docker-compose.yml 檔案中
C)應該使用環境變數來管理敏感設定資訊
D)為了方便開發,應該將所有連接埠都開放給主機
答案:C
解析:在 Docker Compose 設定中使用環境變數來管理敏感資訊(如資料庫憑證)是一種良好的做法。這樣可以增加安全性,同時提高設定的彈性。避免使用 root 使用者執行服務,因為這可能帶來安全風險。同樣,不應該在設定檔中直接寫入敏感資訊,也不應該不必要地開放連接埠,這些都可能增加安全風險。
找出並測試現有 API 中可能存在的安全漏洞,並提出改進建議。
未授權存取
curl http://nodelab.feifei.tw/api/users
參數注入
curl -X GET "http://nodelab.feifei.tw/api/users/1 OR 1=1"
密碼明文傳輸
curl -X POST http://nodelab.feifei.tw/api/users -H "Content-Type: application/json" -d '{"username": "testuser2", "email": "testuser2@example.com", "password": "password123"}'
敏感資訊洩漏
curl http://nodelab.feifei.tw/api/users/1
輸入驗證
curl -X POST http://nodelab.feifei.tw/api/users -H "Content-Type: application/json" -d '{"username": "<script>alert('XSS')</script>", "email": "invalid-email", "password": ""}'
測試項目:[測試的安全問題類型]
API 端點:[測試的 API 路徑]
請求方法:[GET/POST/PUT/DELETE 等]
請求參數:[如果適用]
測試步驟:[詳細說明如何執行測試]
預期結果:[描述安全的 API 應該如何回應]
實際結果:[描述觀察到的回應]
安全問題:[如果發現問題,描述潛在的安全風險]
改進建議:[提出如何修復或改進的建議]