硬編碼值看起來像是捷徑。開發人員需要一個資料庫 URL,直接將其輸入到連接字串中,然後繼續工作。三個月後,URL 發生了變化,更改只在一個文件中生效,卻遺漏了其他四個文件,導致生產環境在凌晨 2 點崩潰。開發人員需要一個 API 金鑰,將其輸入到原始檔案中,提交並推送。六個月後,程式碼庫被克隆,金鑰出現在一個公開的 fork 中,帳號在無人察覺之前就被盜用了。這些並非個例,而是軟體系統中硬編碼值的標準發展軌跡:看似無害的捷徑最終會累積成維護成本、安全事件和部署失敗。
解決方案並不複雜。環境變數、設定檔、依賴注入和集中式常數都是各大主流語言和框架中都已廣泛應用的成熟模式。真正的挑戰在於如何有系統地將這些模式應用到現有程式碼庫中,強制新程式碼也遵循這些模式,並建立起一套機制,以便在問題進入生產環境之前將其發現並解決。下文將詳細介紹實現這些目標所需的所有技術,並提供 Python、Java 和 JavaScript 的範例程式碼。
什麼是硬編碼值?
硬編碼值是指直接嵌入原始程式碼中的字面常數,而不是透過配置、環境變數、資料庫或執行時間輸入提供的。其主要特徵是缺乏間接性:如果更改該值需要修改原始程式碼、重新編譯或重新部署應用程序,則該值是硬編碼的。
常見例子:
- 直接在組態類別中輸入資料庫連接字串
- 來源檔案中的 API 金鑰或存取令牌
- 指向特定環境的服務 URL
- 數值閾值(
if (amount > 5000))沒有任何解釋或外部來源 - 假設伺服器佈局為特定類型,則檔案路徑為:
- 使用者角色字串(
"admin"分散在授權檢查中
硬編碼值存在於所有語言和所有程式碼庫中。傳統的大型主機應用程式直接在 COBOL 程式中編碼特定於環境的資料集名稱、區域代碼和業務閾值。現代微服務在應用程式類別中累積硬編碼的逾時值、重試次數和服務發現 URL。形式隨語言而變化,但問題本質相同。
硬編碼值與常數:有什麼差別?
硬編碼值經常與常數混淆,但二者之間的差異至關重要。常量代表一個穩定、有意設定的、領域層面的事實,它根據定義是正確的,而且不太可能改變: Math.PI, HTTP_STATUS_OK = 200, MAX_RETRY_ATTEMPTS = 3 (如果「三」在所有情況下都確實正確的話)。常量在程式碼中是合適的。它們能提高程式碼的清晰度,防止拼字錯誤,並明確表達意圖。
硬編碼值編碼了關於部署環境、基礎設施或業務規則的假設,而這些假設通常會改變。例如,生產資料庫 URL、API 金鑰、特定區域的稅率、功能標誌狀態等,這些都是偽裝成常數的硬編碼值。判斷方法很簡單:如果某個值在不同的環境、客戶或版本中需要有所不同,那麼它就不是常數,不應該出現在原始碼中。
什麼是軟編碼?
軟編碼是指在執行時間配置值,而不是在編譯時固定值。軟編碼值可以透過設定檔、環境變數、資料庫條目或管理介面進行更改,而無需修改原始程式碼或重新部署應用程式。對於特定於環境的設定、業務參數、功能標誌以及任何需要在開發、測試和生產環境之間有所不同的值,軟編碼都是正確的方法。
硬編碼和軟編碼的區別在於:前者需要修改代碼並部署系統才能更新稅率,而後者只需維運團隊成員在設定檔中更新稅率並重啟服務即可。在大規模應用中,這種區別意味著數千工時的工程師投入和巨大的維運風險。
為什麼硬編碼是一種糟糕的做法
它破壞了可維護性。
硬編碼的值會倍增。一個服務 URL 原本會在一個檔案中硬編碼,但當模組複製到新的整合環境中時,它就會在五個檔案中被硬編碼。一個神奇的數字原本在一個計算中出現,但當相同的邏輯在略有不同的上下文中複製時,它又會在三個不同的上下文中出現。每一次出現都意味著一項獨立的維護任務:找到它、更新它、測試更新,並祈禱沒有遺漏任何實例。
可維護性問題不僅在於工作量,更在於風險。在整個程式碼庫中進行查找替換來更新硬編碼值,相當於一次會影響生產環境的重構操作。任何遺漏的實例都是一個 bug,任何錯誤的替換也是一個 bug。確保變更完全完成的唯一方法是編寫自動化測試,一旦發現任何殘留實例,測試就會失敗。然而,硬編碼值恰恰使得編寫自動化測試變得困難。
它阻礙了測試和持續整合/持續交付。
自動化測試需要在受控環境中運作。如果測試因為連接字串是硬編碼的而連接到生產資料庫,那麼它就不是測試,而是在建置過程中偶然執行的生產作業。如果 CI 管線因為建置伺服器無法存取硬編碼的服務 URL 而中斷,這也不是測試基礎架構的問題,而是硬編碼的問題。
環境變數和組態注入使得同一套測試套件能夠在本地開發、持續整合、預發布和生產環境等具有不同後端服務的環境上運作。如果沒有它們,測試就會變得與特定環境相關、不穩定,最終被棄用。
它會造成嚴重的安全漏洞
硬編碼憑證是軟體安全中最常見且最常被利用的漏洞類型之一。硬編碼的 API 金鑰、資料庫密碼或存取權令牌一旦提交到版本控制系統,即使從目前分支中刪除,也會保留在程式碼庫的歷史記錄中。自動化掃描器會持續掃描公共程式碼庫以查找這些模式。如果密鑰從未輪換,那麼在 18 個月前的提交中找到的密鑰仍然有效。
OWASP明確指出,硬編碼密碼是一個嚴重的安全漏洞(CWE-259、CWE-798)。 NIST指南要求憑證絕對不能儲存在原始碼中。儘管如此,透過硬編碼值外洩憑證仍然是雲端安全事件最常見的原因之一。
風險不僅限於憑證。硬編碼的內部服務 URL 會洩漏基礎設施拓撲結構。硬編碼的使用者角色字串會洩漏授權邏輯。一旦攻擊者知道硬編碼驗證閾值的具體值,就可以繞過這些閾值。這些安全風險乍看之下並不明顯,因此才會不斷累積而未解決。
硬編碼憑證和 API 金鑰:風險最高的類別
硬編碼的密鑰需要單獨處理,因為出錯的後果與其他硬編碼問題截然不同。硬編碼的逾時值會導致漏洞,而硬編碼的 API 金鑰則會導致安全漏洞。
硬編碼憑證是什麼樣子的
蟒蛇
# Python -- hardcoded database credentials (never do this)
connection = psycopg2.connect(
host="prod-db.internal.example.com",
database="accounts",
user="app_service",
password="s3cr3tP@ssword!" # hardcoded secret in source code
)
# Python -- correct approach: load from environment
import os
connection = psycopg2.connect(
host=os.environ["DB_HOST"],
database=os.environ["DB_NAME"],
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"]
)
Java的
// Java -- hardcoded API key (never do this)
private static final String API_KEY = "sk-prod-a1b2c3d4e5f6g7h8i9j0";
// Java -- correct approach: load from environment
private static final String API_KEY = System.getenv("OPENAI_API_KEY");
如何修復硬編碼憑證
環境變數是已部署應用程式中儲存密鑰的標準解決方案。金鑰設定在部署環境(伺服器、容器、Kubernetes Secret 或雲端 Secret 管理器)中,並在執行時間存取。原始碼不包含密鑰值,只包含用於檢索密鑰的機密名稱。
金鑰管理服務,例如 AWS Secrets Manager、HashiCorp Vault、Azure Key Vault 和 Google Secret Manager,是生產級金鑰輪調、稽核和跨服務分發金鑰的方法。應用程式在啟動時(或按需)從密鑰庫中檢索密鑰,而不是從環境變數中檢索,從而實現無需重新部署的密鑰輪換,並產生每次訪問的完整審計日誌。
.env 檔 (使用類似這樣的函式庫) python-dotenv or dotenv 在 Node.js 中)適用於本地開發。它們提供與環境變數相同的接口,但從本機檔案載入資料。關鍵規則: .env 文件必須位於 .gitignore 而且絕對不能犯。
檢測已洩漏的秘密如果某個密鑰已被硬編碼並提交,則應視為已洩漏並立即輪換。從目前分支刪除該值並不會將其從 Git 歷史記錄中刪除。諸如此類的工具 git-filter-branch 或者 BFG Repo Cleaner 可以清除歷史記錄,但提交後最安全的假設是金鑰已暴露。
如何防止API金鑰硬編碼
對於第三方 API 金鑰,硬編碼以外的其他替代方案取決於具體情況:
- 伺服器端應用程式環境變數或金鑰管理服務
- CI / CD管道:管道金鑰變數(GitHub Actions 金鑰、GitLab CI 變數)
- 移動應用API金鑰絕不應該出現在客戶端程式碼中;應該使用後端代理,透過伺服器端儲存的金鑰呼叫API。
- AI代理和LLM集成在代理初始化時從環境變數載入 API 金鑰,切勿在提示或函數呼叫中將其作為硬編碼字串傳遞。
IBM i 上的 RPG 安全編碼遵循相同的原則:外部資料區、資料佇列或系統值應該保存特定於環境的連線參數,而不是將它們編碼到程式原始碼中。
如何防止硬編碼值:用程式碼完善模式
環境變量
環境變數是最通用的解決方案。所有主流作業系統、雲端平台、容器系統和部署框架都支援環境變數。
蟒蛇
# Python: load all configuration from environment
import os
from dotenv import load_dotenv
load_dotenv() # loads .env file in development; ignored in production
DATABASE_URL = os.environ["DATABASE_URL"]
API_BASE_URL = os.environ.get("API_BASE_URL", "https://api.example.com")
MAX_RETRIES = int(os.environ.get("MAX_RETRIES", "3"))
DEBUG_MODE = os.environ.get("DEBUG", "false").lower() == "true"
JavaScript的
// Node.js: access environment variables
require('dotenv').config(); // load .env in development
const config = {
dbUrl: process.env.DATABASE_URL,
apiKey: process.env.API_KEY,
port: parseInt(process.env.PORT, 10) || 3000,
debug: process.env.NODE_ENV !== 'production',
};
Java的
// Java: environment variables via System.getenv()
public class AppConfig {
public static final String DB_URL = System.getenv("DATABASE_URL");
public static final String API_KEY = System.getenv("THIRD_PARTY_API_KEY");
public static final int TIMEOUT = Integer.parseInt(
Optional.ofNullable(System.getenv("REQUEST_TIMEOUT_MS")).orElse("5000")
);
}
配置文件
設定檔將特定於環境但非機密的值、服務名稱、功能標誌、分頁大小、快取 TTL 等外部化。
雅姆
# config/production.yaml
database:
host: prod-db.internal
port: 5432
pool_size: 20
api:
base_url: https://api.partner.com/v2
timeout_ms: 3000
max_retries: 3
features:
new_checkout: true
beta_dashboard: false
蟒蛇
# Load at startup; never hardcode the values it contains
import yaml
with open(f"config/{os.environ['APP_ENV']}.yaml") as f:
config = yaml.safe_load(f)
timeout = config["api"]["timeout_ms"]
依賴注入
依賴注入將配置值傳遞給元件,而不是讓元件建立或檢索自己的配置。這使得組件可以獨立測試,並且可以針對每個環境進行配置。
Java的
// Spring Boot: inject configuration via @Value
@Service
public class PaymentService {
@Value("${payment.api.url}")
private String apiUrl;
@Value("${payment.api.timeout-ms}")
private int timeoutMs;
// No hardcoded URL or timeout anywhere in this class
public PaymentResult process(Payment payment) {
// uses apiUrl and timeoutMs injected from application.properties
}
}
蟒蛇
# Python: simple constructor injection
class EmailService:
def __init__(self, smtp_host: str, smtp_port: int, api_key: str):
self.smtp_host = smtp_host
self.smtp_port = smtp_port
self.api_key = api_key
# Wire at application startup, reading from environment
email_service = EmailService(
smtp_host=os.environ["SMTP_HOST"],
smtp_port=int(os.environ["SMTP_PORT"]),
api_key=os.environ["EMAIL_API_KEY"],
)
集中式常數與枚舉
真正由設計決定的值、HTTP 狀態代碼、域特定狀態、協定標識符,應該放在一個單獨的、命名的常數模組中,而不是作為字串字面量分散在一起。
蟒蛇
# Python: centralised constants
from enum import Enum
class OrderStatus(Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
# Usage: no magic strings scattered across modules
if order.status == OrderStatus.CONFIRMED:
notify_warehouse(order)
Java的
// Java: enum with business meaning
public enum UserRole {
ADMIN("admin"),
EDITOR("editor"),
VIEWER("viewer");
private final String value;
UserRole(String value) { this.value = value; }
public String getValue() { return value; }
}
// Replaces scattered "admin", "editor", "viewer" strings
if (user.getRole() == UserRole.ADMIN) {
grantFullAccess(user);
}
特定語言中的硬編碼
Python中的硬編碼是什麼?
在 Python 中,硬編碼最常見的表現形式是字串字面量(用於 URL 和憑證)、業務邏輯中的數值常數以及假定特定目錄結構的檔案路徑。 os.environ 以及 python-dotenv 是基於環境的配置的標準機制。 configparser 此模組處理 INI 格式的設定檔。 pydantic-settings 它提供基於環境變數的類型驗證配置,並帶有預設值,這是生產 Python 服務的建議方法。
偵測 Python 中的硬編碼值:靜態分析工具,包括 Bandit(用於偵測密碼和金鑰等安全相關的硬編碼)、Pylint 和 Semgrep,可以自動辨識硬編碼的憑證和魔數。
Java中的硬編碼是什麼?
在 Java 中,硬編碼值通常以以下形式出現: private static final String 領域 @Value Spring Boot 的註解包含應該外部化的內聯預設值,以及業務邏輯中的字面量。 application.properties 以及 application.yml 文件是標準的外部化機制。 @ConfigurationProperties帶有註解的類別可以從 YAML 或屬性檔案中提供類型安全性、經過驗證的設定物件。
偵測 Java 中的硬編碼值:SpotBugs 配合 Find Security Bugs 外掛程式可以偵測硬編碼的憑證。 SonarQube 可以標記魔數、硬編碼的 URL 和硬編碼的密碼。 SMART TS XL企業分析涵蓋 Java 程式碼庫以及 COBOL 和其他遺留語言。
傳統代碼和大型主機代碼中的硬編碼
IBM 大型主機和中型機系統上的 COBOL、RPG 和 PL/I 程式具有特定的硬編碼模式:資料集名稱嵌入在 DD 語句中,環境識別碼位於程式邏輯中,業務閾值直接編譯到程式中。要將這些值外部化,需要使用能夠識別大型主機的工具:系統參數 (SYSPARM)、外部資料區、DB2 中的配置表以及參數化的 JCL。需要能夠理解大型主機語言的靜態分析工具才能大規模地偵測這些模式。
真實世界的重構:從硬式編碼到可配置
三階段重構方法
第一階段:發現。對整個程式碼庫進行靜態分析,產生硬編碼值清單,並按類型(憑證、URL、業務邏輯、魔法數字)和風險等級進行分組。優先處理憑證和生產環境特有的值。
第二階段:依類別外部化。首先處理憑證(風險最高):將所有金鑰移至環境變數或金鑰管理員。然後處理特定於環境的 URL 和服務名稱。接著處理業務邏輯閾值。最後,透過將“魔數”替換為命名常數或配置值來處理它們。
第三階段:強制執行。在持續整合 (CI) 管線中新增靜態分析檢查,以偵測新新增的硬編碼憑證。新增代碼檢查規則,標記「魔數」。新增程式碼審查清單項目。目標是使添加硬編碼值的難度高於使用正確的模式。
識別遺留項目中的硬編碼值
在那些多年來累積了硬編碼值的遺留項目中:
- 使用
grep或在儲存庫中搜尋常見模式:連接字串,password,apikeyIP 位址模式和知名服務名稱 - 運行靜態分析工具(Bandit、SonarQube、Semgrep、 SMART TS XL)無需人工逐個文件審核即可獲得完整的庫存清單
- 檢查 Git 歷史記錄,尋找先前提交後又刪除的金鑰,它們仍然可以在倉庫歷史記錄中存取。
- 尋找在多個文件中出現的相同值,這些值可能是集中化的候選值。
防止引入硬編碼值
- 預提交鉤子執行憑證掃描(例如,
detect-secrets,git-secrets,truffleHog在每次提交到達存儲庫之前進行此操作 - CI管道門:建置失敗,其中包含新引入的靜態分析結果(例如憑證或魔數)。
- 程式碼審查清單:團隊審查過程中配置外部化的明確項目
- 開發者入職將正確的模式設為預設值,提供一個已經使用環境變數的項目模板,以及
.env.example文件
SMART TS XL 大規模消除硬編碼值
對於小型程式碼庫,手動搜尋和 grep 命令足以找到顯而易見的異常情況。但在涉及 COBOL、Java、Python、.NET、RPG 和 SQL 等多種語言、數百萬行程式碼的企業級系統中,要全面偵測硬編碼值,則需要進行自動化的結構分析。 SMART TS XL 同時對所有這些語言進行靜態分析,建構一個統一的交叉引用模型,可以識別:
- 連接參數和身份驗證呼叫中的字面字串值,被標記為潛在的憑證洩露風險
- 將業務邏輯中的「魔法數字」與可設定的基準進行比較,以識別跨模組不一致或在不同位置出現不同值的數字。
- 多個文件中出現的重複字面值,雖然邏輯目的相同,但這表示存在集中化的機會。
- COBOL 程式中用於編碼特定於環境的標識符的值通常透過 JCL 參數進行管理。
- 系統資料流路徑圖,從硬編碼值開始,顯示哪些下游操作依賴這些值,然後再做出重構決策。
靜態程式碼分析功能提供清單。影響分析功能顯示替換硬編碼值後將受影響的方面。遺留系統現代化功能將此擴展到跨語言和跨平台場景,在這種情況下,相同的邏輯值在大型主機程式和 Java 服務中分別以硬編碼形式存在,需要進行協調的修復,而不是孤立的修復。
對於專門管理證券債務的團隊而言, SMART TS XL的硬編碼憑證偵測功能已整合到 CI/CD 管道中作為建置閘:引入硬編碼金鑰的新提交會自動導致建置失敗,防止程式碼到達可能暴露的儲存庫。
硬編碼是紀律問題,不是知識問題。
大多數將值硬編碼到程式碼中的開發者都知道這樣做是不對的。環境變數的存在、設定檔才是正確的做法、以及憑證永遠不應該出現在原始碼中,這些知識早已廣為人知,也廣為理解。問題不在於知識本身,而在於壓力下的自律。
交付期限使得環境變數設定顯得繁瑣。遺留程式碼庫使得外部化比保留現有值風險更大。大型團隊缺乏統一的新員工入職流程,使得新開發人員很容易複製錯誤的現有模式。因此,在組織層級解決硬編碼問題需要的不僅僅是文件:它還需要自動化強制執行機制,使錯誤的模式比正確的模式更難被採用。
提交前鉤子會拒絕包含敏感資訊的提交,持續整合管線會在遇到新的「魔數」時失敗,程式碼審查清單會明確詢問配置外部化情況,靜態分析工具會揭示程式碼庫中所有現有硬編碼值的全部範圍:這些機制彌合了「知道正確方法」和「一致地應用正確方法」之間的差距。本文中的程式碼範例和模式提供了技術基礎,而強制執行機制則確保它們能夠真正發揮作用。


