生成AI(ChatGPT、Claude、GitHub Copilot等)寫程式碼的機會激增,開發速度劇烈提升,但「可運行的程式碼」和「安全的程式碼」是兩回事。特別是在生產環境中,需要考量性能、安全性和可維護性。
我們精選了AI容易寫出來(或寫出來後令人厭惡的)15個危險模式。每個模式我們都配上了「危險的地方」和「修正範例」。
本文的目標讀者
◇ 使用生成AI寫Python程式碼的工程師
◇ 曾經對「可運行但上生產環境會不會有問題?」感到不安的人
◇ 想要在團隊開發中安全使用AI生成的程式碼的人
暫且來說,「記下來也許會有幫助!」。
首先是防止AI輸出print()
滿天飛的程式碼的基本設定。
# logging 基準線(可複製使用)
import logging
import sys
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s - %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger(__name__)
# 使用範例
logger.info("處理開始")
logger.debug("除錯資訊: %s", data)
logger.error("發生錯誤: %s", error)
except
AI經常會寫出這樣的程式碼,以「先讓它運行」為目的。
# 惡劣 - AI常寫的模式
try:
result = api_call()
process_data(result)
except Exception: # 或者裸exepct:
pass # 忽略錯誤
危險的地方是什麼?
[ 修正範例 ]
# 良好
import logging
logger = logging.getLogger(__name__)
try:
result = api_call()
process_data(result)
except APIError as e:
logger.exception("API呼叫失敗: %s", e)
raise # 重新引發不隱藏錯誤
except DataProcessingError as e:
logger.exception("資料處理失敗: %s", e)
# 根據需要返回預設值
return default_response()
這也是AI經常寫出來的典型錯誤溫床。即使不是AI,初學者也會常犯。
# 惡劣
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
# 執行時...
list1 = add_item("a") # ["a"]
list2 = add_item("b") # ["a", "b"] ← 前一次的結果保留!
危險的地方是什麼?
[ 修正範例 ]
# 良好
def add_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
AI為了「只要能動就好」而隨意使用危險的函數。
# 惡劣 - 都是危險
eval(user_input) # 任意程式碼執行
exec(code_string) # 同上
subprocess.run(cmd, shell=True) # 命令注入
yaml.load(data, Loader=yaml.Loader) # 任意對象執行
pickle.loads(untrusted_data) # 任意程式碼執行
requests.get(url, verify=False) # 關閉TLS驗證
危險的地方是什麼?
Pickle是Python標準的便捷機制,但使用於從外部傳入的數據時會有任意程式碼執行風險。請避免在「自己儲存的資料」以外的情況下使用。
requests.get(..., verify=False) 會關閉SSL驗證,存在中間人攻擊的風險。應限於開發調試或內部代理環境使用。
[ 修正範例 ]
# 良好
import ast
import json
import subprocess
import yaml
import requests
# JSON一定要使用json庫
result = json.loads('{"key": "value"}')
# 僅限Python字面量的ast.literal_eval
config = ast.literal_eval("{'debug': True, 'port': 8080}")
# 不透過shell直接執行(必須設定timeout)
completed = subprocess.run(
["convert", input_file, output_file],
check=True, timeout=30, capture_output=True, text=True
)
# 安全的YAML加載(僅信任的輸入)
data = yaml.safe_load(yaml_string)
# 網絡I/O必須設定timeout
response = requests.get(url, timeout=10) # verify=True為預設值
# Pickle設計允許任意程式碼執行,請在外部輸入時不用
# 替代:JSON、msgpack、protobuf等
這是經典但至今仍常見的AI錯誤。
# 惡劣
import sqlite3
def get_user(name):
cur.execute(f"SELECT * FROM users WHERE name='{name}'")
def read_file(filename):
return open(f"/uploads/{filename}").read()
危險的地方是什麼?
../../../etc/passwd
訪問機密檔案[ 修正範例 ]
# 良好
from pathlib import Path
def get_user(name):
cur.execute("SELECT * FROM users WHERE name = ?", (name,))
def read_file(filename):
# 明確編碼 + 路徑正規化以防避開目錄
base = Path("uploads").resolve()
target = (base / Path(filename).name).resolve()
# Python 3.11+: 嚴格的路徑檢查
if not target.is_relative_to(base):
raise ValueError("無效路徑")
return target.read_text(encoding="utf-8")
AI經常會這麼做以便於除錯。
# 惡劣
logger.info(f"API代碼: {token}")
logger.debug(f"密碼: {password}")
print(f"信用卡資訊: {credit_card}")
危險的地方是什麼?
[ 修正範例 ]
# 良好
logger.info("認證完成") # 不輸出秘碼
logger.debug("用戶ID: %s", user.id) # 僅ID
# 信用卡資訊僅顯示最後4位
logger.info("卡片註冊: ****%s", card_number[-4:])
AI對於時間處理非常不擅長,經常寫出這種程式碼。
# 惡劣
from datetime import datetime
now = datetime.now() # 沒有時區的datetime
expiry = now + timedelta(days=1)
危險的地方是什麼?
[ 修正範例 ]
# 良好
from datetime import datetime, timezone, timedelta
# 必須使用時區
now = datetime.now(timezone.utc)
expiry = now + timedelta(days=1)
# 儲存UTC,顯示時進行本地轉換
def format_for_user(utc_time, user_timezone):
local_time = utc_time.astimezone(user_timezone)
return local_time.strftime('%Y-%m-%d %H:%M')
數值計算中,AI常常受到這類模式的困擾。
# 惡劣
score = 0.1 + 0.1 + 0.1
if score == 0.3: # 可能結果為False
print("正確")
危險的地方是什麼?
[ 修正範例 ]
# 良好
import math
score = 0.1 + 0.1 + 0.1
if math.isclose(score, 0.3, rel_tol=1e-9):
print("正確")
# 金錢計算要使用Decimal(明確的四捨五入策略)
from decimal import Decimal, getcontext, ROUND_HALF_UP
getcontext().rounding = ROUND_HALF_UP
price = Decimal('19.99')
tax = Decimal('0.08')
total = price * (1 + tax)
在數據分析中,AI常常寫出這種程式碼,但會產生警告。
# 惡劣 - 會有SettingWithCopyWarning
df[df.score > 80]['grade'] = 'A'
危險的地方是什麼?
[ 修正範例 ]
# 良好
mask = df.score > 80
df.loc[mask, 'grade'] = 'A'
# 或者
df = df.copy() # 明確複製後再操作
df[df.score > 80]['grade'] = 'A'
使用async/await但中間內容卻是阻塞的。
# 惡劣
import requests
async def fetch_data(url):
response = requests.get(url) # 阻塞
return response.json()
危險的地方是什麼?
[ 修正範例 ]
# 良好
import httpx
async def fetch_data(url):
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=10)
response.raise_for_status()
return response.json()
httpx是第三方庫。
使用前請不要忘記pip install httpx。
AI常常會忘記使用with
語句。
# 惡劣
def process_file(path):
f = open(path)
data = f.read() # 忘記close
return data.upper()
危險的地方是什麼?
[ 修正範例 ]
# 良好
def process_file(path):
with open(path, encoding="utf-8") as f:
data = f.read()
return data.upper()
# 多個資源的情況
def copy_file(src, dst):
with open(src, encoding="utf-8") as f_in, \
open(dst, 'w', encoding="utf-8") as f_out:
f_out.write(f_in.read())
設定寫死在代碼中的AI。
# 惡劣
API_URL = "https://api.production.com"
MAX_RETRIES = 3
SECRET_KEY = "abc123"
危險的地方是什麼?
[ 修正範例 ]
# 良好
import os
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
api_url: str = "http://localhost:8000"
max_retries: int = 3
secret_key: str
class Config:
env_file = ".env"
settings = Settings()
pydantic-settings是第三方庫。
使用前請不要忘記pip install pydantic-settings。
AI任意創建「看起來存在」的API。
# 惡劣 - pandas.DataFrame.save_to_csv()不存在
df.save_to_csv("output.csv")
# 惡劣 - requests.get()中的header參數不存在
requests.get(url, header={"Authorization": token})
危險的地方是什麼?
[ 修正範例 ]
# 良好 - 正確的API名稱
df.to_csv("output.csv", index=False)
# 良好 - 正確的引數名稱
requests.get(url, headers={"Authorization": f"Bearer {token}"})
不明的API或行為,務必參考官方文檔。
AI生成的程式碼中可能混入「不存在的方法名」「錯誤的引數」。
AI寫的「看似能運行但很慢」的程式碼。
# 惡劣 - 字串連接
result = ""
for item in large_list:
result += str(item) # 每次創建新字符串
# 惡劣 - 全部文件讀取
with open("huge_file.txt") as f:
lines = f.read().split('\n') # 吞掉所有記憶體
# 惡劣 - 雙重循環
matches = []
for a in list1:
for b in list2: # O(n²)
if a.id == b.id:
matches.append((a, b))
危險的地方是什麼?
[ 修正範例 ]
# 良好
result = "".join(str(item) for item in large_list)
# 良好 - 串流處理
with open("huge_file.txt") as f:
for line in f: # 逐行處理
process_line(line.strip())
# 良好 - 使用字典加速
lookup = {b.id: b for b in list2}
matches = [(a, lookup[a.id]) for a in list1 if a.id in lookup]
使用random
用於加密目的的AI。
# 惡劣 - 不適用於安全目的
import random
token = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=32))
危險的地方是什麼?
[ 修正範例 ]
# 良好 - 加密學上安全
import secrets
token = secrets.token_urlsafe(32)
password = secrets.token_hex(16)
# 安全比較(防止時間攻擊)
from hmac import compare_digest
if compare_digest(provided_token, expected_token):
authenticate_user()
性能較差,同時也存在安全風險。
# 惡劣
logger.info(f"用戶 {user.name} 已登錄")
logger.debug(f"處理時間: {elapsed:.2f}秒")
危險的地方是什麼?
[ 修正範例 ]
# 良好 - 延遲格式化
logger.info("用戶 %s 已登錄", user.name)
logger.debug("處理時間: %.2f秒", elapsed)
# 當日誌等級為off時不會執行格式化處理
為了安全地使用AI生成的程式碼,建議至少導入以下工具。
[tool.ruff]
line-length = 100
lint.select = [
"E", "F", # pycodestyle + pyflakes
"B", # bugbear (常見錯誤模式)
"I", # isort
"UP", # pyupgrade
"NPY", # NumPy
"S", # bandit (安全)
"C4", # comprehension
]
[tool.black]
line-length = 100
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 24.8.0
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
hooks:
- id: ruff
args: ["--fix"]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.2
hooks:
- id: mypy
- repo: https://github.com/PyCQA/bandit
rev: 1.7.9
hooks:
- id: bandit
args: ["-r", "."]
我們將常用的修正模式模板化。
# 轉換前
subprocess.run(f"convert {input_file} {output_file}", shell=True)
# 轉換後
completed = subprocess.run([
"convert",
input_file,
output_file
], check=True, timeout=30, capture_output=True, text=True)
# 根據需要記錄輸出
logger.debug("轉換完成: %s", completed.stdout)
# 轉換前
DATABASE_URL = "postgresql://user:pass@localhost/db"
# 轉換後
import os
DATABASE_URL = os.environ.get(
"DATABASE_URL",
"postgresql://user:pass@localhost/db"
)
import time
import logging
from typing import Any, Dict
logger = logging.getLogger(__name__)
def api_call_with_retry(
url: str,
retries: int = 3,
backoff: float = 0.5,
timeout: int = 10
) -> Dict[str, Any]:
"""帶重試功能的API呼叫"""
import httpx
for attempt in range(retries):
try:
with httpx.Client(timeout=timeout) as client:
response = client.get(url)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
logger.warning(
"API呼叫失敗 (嘗試 %d/%d): %s",
attempt + 1, retries, e
)
if attempt == retries - 1: # 最後一次嘗試
raise
time.sleep(backoff * (2 ** attempt)) # 指數回退
raise RuntimeError("達到最大重試次數")
查看diff時,首先檢查的項目。
立即紅旗
except:
或 except Exception:
eval(
, exec(
, shell=True
, verify=False
"/path/" + user_input
的路徑連接datetime.now()
(無時區)print()
而未使用loggingrequests.get()
時未指定timeout需注意模式
global
關鍵字time.sleep()
、requests.get()
等)最低限度的測試代碼模板。
# tests/test_safety.py
import math
import pytest
def test_float_comparison():
"""浮點數比較測試"""
result = 0.1 + 0.1 + 0.1
assert math.isclose(result, 0.3, rel_tol=1e-9)
def test_mutable_default_args():
"""可變預設引數測試"""
from myapp import add_item
list1 = add_item("a")
list2 = add_item("b")
# 確認互不影響
assert list1 == ["a"]
assert list2 == ["b"]
def test_exception_handling():
"""例外處理的測試"""
from myapp import risky_function
with pytest.raises(SpecificError):
risky_function(invalid_input)
def test_security_config():
"""安全設定的測試"""
import myapp.settings
# 確保生產環境下DEBUG禁用
assert not myapp.settings.DEBUG
# 確保秘密金鑰不為預設值
assert myapp.settings.SECRET_KEY != "changeme"
生成AI確實加速了開發進程,但「可運行的程式碼」和「安全的程式碼」是兩件事。特別是在將程式碼上線到生產環境之前,請務必使用這份檢查清單進行最低限度的安全確認。
原文出處:https://qiita.com/Sakai_path/items/d4ec1e848672033ca256