前言

生成AI(ChatGPT、Claude、GitHub Copilot等)寫程式碼的機會激增,開發速度劇烈提升,但「可運行的程式碼」和「安全的程式碼」是兩回事。特別是在生產環境中,需要考量性能、安全性和可維護性。

我們精選了AI容易寫出來(或寫出來後令人厭惡的)15個危險模式。每個模式我們都配上了「危險的地方」和「修正範例」。

本文的目標讀者
◇ 使用生成AI寫Python程式碼的工程師
◇ 曾經對「可運行但上生產環境會不會有問題?」感到不安的人
◇ 想要在團隊開發中安全使用AI生成的程式碼的人

暫且來說,「記下來也許會有幫助!」。

立即修正的危險模式前15名

基本日誌設定(擺脫print濫用)

首先是防止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)

1. 例外的忽略與雜湊的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()

2. 可變預設引數的陷阱

這也是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

3. 安全危險API的輕易使用

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等

4. SQL注入及路徑操作

這是經典但至今仍常見的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()

危險的地方是什麼?

  • SQL注入:任意SQL語句會被執行
  • 路徑穿越:透過../../../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")

5. 日誌中輸出秘密資訊

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:])

6. 時間與時區的無自覺

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')

7. 浮動小數點的直接比較

數值計算中,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)

8. Pandas的警告被忽視的鏈式賦值

在數據分析中,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'

9. 非同步處理中的阻塞I/O

使用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。

10. 資源未釋放

AI常常會忘記使用with語句。

# 惡劣
def process_file(path):
    f = open(path)
    data = f.read()  # 忘記close
    return data.upper()

危險的地方是什麼?

  • 文件句柄泄露
  • 遇到「too many open files」錯誤時,應用程式停止
  • 內存泄漏或者資源枯竭

[ 修正範例 ]

# 良好
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())

11. 硬編碼的值

設定寫死在代碼中的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。

12. 不存在的引數或函數名(幻覺)

AI任意創建「看起來存在」的API。

# 惡劣 - pandas.DataFrame.save_to_csv()不存在
df.save_to_csv("output.csv")

# 惡劣 - requests.get()中的header參數不存在
requests.get(url, header={"Authorization": token})

危險的地方是什麼?

  • 執行時會因為AttributeError立刻崩潰
  • 如果未經測試,則在部署後會暴露
  • 錯誤API規範會導致預期外的行為

[ 修正範例 ]

# 良好 - 正確的API名稱
df.to_csv("output.csv", index=False)

# 良好 - 正確的引數名稱
requests.get(url, headers={"Authorization": f"Bearer {token}"})

不明的API或行為,務必參考官方文檔。
AI生成的程式碼中可能混入「不存在的方法名」「錯誤的引數」。

13. 性能地雷

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))

危險的地方是什麼?

  • 字串連接:隨著數據量增加,內存使用量暴增
  • 大文件讀取:可能因OutOfMemoryError導致應用程式停止
  • 雙重循環:處理時間以平方增長,導致無法響應

[ 修正範例 ]

# 良好
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]

14. 用於安全的普通隨機數

使用random用於加密目的的AI。

# 惡劣 - 不適用於安全目的
import random
token = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=32))

危險的地方是什麼?

  • 可預測的隨機數使得token容易被猜測
  • 會產生會話劫持與冒名頂替的風險
  • 加密強度不足,易受到攻擊

[ 修正範例 ]

# 良好 - 加密學上安全
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()

15. 日誌輸出時使用f字串

性能較差,同時也存在安全風險。

# 惡劣
logger.info(f"用戶 {user.name} 已登錄")
logger.debug(f"處理時間: {elapsed:.2f}秒")

危險的地方是什麼?

  • 日誌等級為off時,格式化操作仍然會執行
  • 大量日誌輸出時,CPU使用率上升
  • 日誌格式處理成為性能瓶頸

[ 修正範例 ]

# 良好 - 延遲格式化
logger.info("用戶 %s 已登錄", user.name)
logger.debug("處理時間: %.2f秒", elapsed)

# 當日誌等級為off時不會執行格式化處理

最低限度的防護設置

為了安全地使用AI生成的程式碼,建議至少導入以下工具。

pyproject.toml設定範例

[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設定範例

# .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", "."]

AI產生的程式碼安全轉換模板

我們將常用的修正模式模板化。

subprocess安全化模板

# 轉換前
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"
)

帶重試的API呼叫模板

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()而未使用logging
  • requests.get()時未指定timeout

需注意模式

  • 未使用的變數或import
  • global關鍵字
  • requirements.txt中未固定版本(必須依賴鎖定)
  • 無官方的引數名或方法名
  • 在非同步函數內的同步I/O(如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


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝11   💬6   ❤️3
447
🥈
我愛JS
📝1   💬7   ❤️4
104
🥉
AppleLily
📝1   💬4   ❤️1
58
#4
💬1  
5
#5
xxuan
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次