什麼是存取控制失效?
存取控制失效(Broken Access Control)是 OWASP Top 10 2021 排名第一的網路安全風險。當應用程式未能正確限制使用者對資源的存取權限時,攻擊者可以存取未經授權的功能或資料。
存取控制負責確保使用者只能在其權限範圍內執行操作。當這些控制機制失效時,可能導致:
- 未經授權存取敏感資料
- 修改或刪除其他使用者的資料
- 執行超出權限的功能
- 權限提升至管理員等級
常見漏洞類型
1. IDOR(不安全的直接物件參照)
IDOR(Insecure Direct Object Reference)是最常見的存取控制漏洞之一。當應用程式直接使用使用者提供的輸入來存取物件時,攻擊者可以透過修改參數來存取其他使用者的資料。
漏洞範例:
1
2
3
4
5
| # 原始請求 - 查看自己的訂單
GET /api/orders/12345
# 攻擊者修改訂單 ID
GET /api/orders/12346
|
易受攻擊的程式碼:
1
2
3
4
5
| @app.route('/api/orders/<order_id>')
def get_order(order_id):
# 危險:未驗證使用者是否有權限存取此訂單
order = Order.query.get(order_id)
return jsonify(order.to_dict())
|
修復後的程式碼:
1
2
3
4
5
6
7
8
9
10
| @app.route('/api/orders/<order_id>')
@login_required
def get_order(order_id):
order = Order.query.get(order_id)
# 驗證訂單是否屬於當前使用者
if order.user_id != current_user.id:
abort(403) # 禁止存取
return jsonify(order.to_dict())
|
2. 權限提升漏洞
權限提升分為兩種類型:
垂直權限提升: 一般使用者獲得管理員權限
水平權限提升: 使用者存取其他同級使用者的資料
漏洞範例 - 隱藏的管理功能:
1
2
| <!-- 前端隱藏管理連結,但後端未驗證權限 -->
<a href="/admin/users" style="display:none;">管理使用者</a>
|
易受攻擊的程式碼:
1
2
3
4
5
6
7
| // Node.js Express 範例
app.post('/admin/users/delete', (req, res) => {
// 危險:僅依賴前端隱藏,未驗證後端權限
const userId = req.body.userId;
User.deleteById(userId);
res.json({ success: true });
});
|
修復後的程式碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 加入角色驗證中介軟體
const requireAdmin = (req, res, next) => {
if (!req.user || req.user.role !== 'admin') {
return res.status(403).json({ error: '權限不足' });
}
next();
};
app.post('/admin/users/delete', requireAdmin, (req, res) => {
const userId = req.body.userId;
User.deleteById(userId);
res.json({ success: true });
});
|
3. 路徑穿越攻擊
路徑穿越(Path Traversal)允許攻擊者存取應用程式目錄外的檔案。
攻擊範例:
1
2
3
4
5
| # 原始請求
GET /download?file=report.pdf
# 攻擊請求
GET /download?file=../../../etc/passwd
|
易受攻擊的程式碼:
1
2
3
4
5
| @app.route('/download')
def download_file():
filename = request.args.get('file')
# 危險:直接使用使用者輸入
return send_file(f'/var/www/uploads/{filename}')
|
修復後的程式碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| import os
from werkzeug.utils import secure_filename
@app.route('/download')
def download_file():
filename = request.args.get('file')
# 清理檔案名稱
safe_filename = secure_filename(filename)
# 建構完整路徑並驗證
base_path = '/var/www/uploads'
file_path = os.path.realpath(os.path.join(base_path, safe_filename))
# 確保路徑在允許的目錄內
if not file_path.startswith(base_path):
abort(403)
if not os.path.exists(file_path):
abort(404)
return send_file(file_path)
|
4. 缺少功能層級存取控制
當應用程式未對每個功能端點實施適當的權限檢查時,攻擊者可以直接存取受限功能。
易受攻擊的 API 設計:
1
2
3
4
5
6
| # 危險:API 端點缺少權限驗證
@app.route('/api/admin/settings', methods=['POST'])
def update_settings():
settings = request.json
SystemSettings.update(settings)
return jsonify({'status': 'updated'})
|
修復後的程式碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| from functools import wraps
def require_permission(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.has_permission(permission):
return jsonify({'error': '權限不足'}), 403
return f(*args, **kwargs)
return decorated_function
return decorator
@app.route('/api/admin/settings', methods=['POST'])
@login_required
@require_permission('admin:settings:write')
def update_settings():
settings = request.json
SystemSettings.update(settings)
return jsonify({'status': 'updated'})
|
測試方法
手動測試步驟
識別存取控制點
測試 IDOR 漏洞
- 修改 URL 中的 ID 參數
- 嘗試存取其他使用者的資源
測試權限提升
- 使用低權限帳號存取管理功能
- 修改請求中的角色或權限參數
測試路徑穿越
- 在檔案參數中注入
../ 序列 - 嘗試存取系統敏感檔案
自動化測試工具
1
2
3
4
5
6
7
8
| # 使用 Burp Suite 進行自動化掃描
# 使用 OWASP ZAP 進行被動掃描
# 使用 ffuf 進行端點發現
ffuf -u https://target.com/FUZZ -w /path/to/wordlist.txt
# 使用 nuclei 進行漏洞掃描
nuclei -u https://target.com -t exposures/
|
防護措施
1. 實施最小權限原則
1
2
3
4
5
6
7
8
9
10
| class Permission:
READ = 'read'
WRITE = 'write'
DELETE = 'delete'
ADMIN = 'admin'
class Role:
GUEST = [Permission.READ]
USER = [Permission.READ, Permission.WRITE]
ADMIN = [Permission.READ, Permission.WRITE, Permission.DELETE, Permission.ADMIN]
|
2. 在伺服器端驗證所有請求
1
2
3
4
5
6
7
8
9
10
11
| def verify_resource_access(user, resource):
"""
在每個請求中驗證使用者對資源的存取權限
"""
# 檢查資源所有權
if resource.owner_id != user.id:
# 檢查是否有共享權限
if not resource.is_shared_with(user):
raise PermissionDenied("無權存取此資源")
return True
|
3. 使用間接物件參照
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import hashlib
import secrets
def generate_secure_token(resource_id, user_id):
"""
產生安全的資源存取令牌
"""
secret = secrets.token_hex(32)
token = hashlib.sha256(
f"{resource_id}:{user_id}:{secret}".encode()
).hexdigest()
# 將令牌與資源映射儲存在資料庫
TokenMapping.create(token=token, resource_id=resource_id, user_id=user_id)
return token
|
4. 記錄存取控制失敗事件
1
2
3
4
5
6
7
8
9
| import logging
security_logger = logging.getLogger('security')
def log_access_denied(user, resource, action):
security_logger.warning(
f"存取被拒絕 - 使用者: {user.id}, 資源: {resource.id}, 動作: {action}, "
f"IP: {request.remote_addr}, 時間: {datetime.now()}"
)
|
安全檢查清單
參考資料