forked from wangziqi/gongxue-base
chore: harden production readiness gates
This commit is contained in:
@@ -53,6 +53,8 @@ const DEFAULT_PLATFORM_ADMIN_API_KEY = 'local-platform-admin-key';
|
||||
const DEFAULT_MAX_JSON_BODY_BYTES = 1024 * 1024;
|
||||
const DEFAULT_MAX_IMPORT_JSON_BODY_BYTES = 10 * 1024 * 1024;
|
||||
const HARD_MAX_JSON_BODY_BYTES = 50 * 1024 * 1024;
|
||||
const PRODUCTION_SMS_PROVIDERS = new Set(['aliyun', 'aliyun-sms', 'aliyun_sms', 'tencent', 'tencent-sms', 'tencent_sms']);
|
||||
const PRODUCTION_STORAGE_PROVIDERS = new Set(['aliyun_oss', 'tencent_cos', 'supabase_storage']);
|
||||
|
||||
function boundedBytes(key: string, fallback: number, hardMax = HARD_MAX_JSON_BODY_BYTES) {
|
||||
const value = envNumber(key, fallback);
|
||||
@@ -71,12 +73,37 @@ function isUnsafeSecret(value: string, defaultValue: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function hostFromUrl(value: string) {
|
||||
try {
|
||||
return new URL(value).hostname.toLowerCase();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isLocalHost(hostname: string) {
|
||||
return ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(hostname);
|
||||
}
|
||||
|
||||
function isHttpsProductionUrl(value: string) {
|
||||
if (!value.trim()) return false;
|
||||
const host = hostFromUrl(value);
|
||||
return value.startsWith('https://') && Boolean(host) && !isLocalHost(host);
|
||||
}
|
||||
|
||||
function isAllowedHost(value: string, allowedHosts: string[]) {
|
||||
const host = hostFromUrl(value);
|
||||
return allowedHosts.some(allowed => host === allowed || host.endsWith(`.${allowed}`));
|
||||
}
|
||||
|
||||
function validateProductionConfig(nextConfig: ApiConfig) {
|
||||
if (!nextConfig.isProduction) return;
|
||||
|
||||
const failures: string[] = [];
|
||||
if (nextConfig.corsOrigins.includes('*')) failures.push('CORS_ORIGIN must not include * in production');
|
||||
if (nextConfig.authSmsProvider === 'mock') failures.push('AUTH_SMS_PROVIDER=mock is not allowed in production');
|
||||
if (!PRODUCTION_SMS_PROVIDERS.has(nextConfig.authSmsProvider.trim().toLowerCase())) {
|
||||
failures.push('AUTH_SMS_PROVIDER must be aliyun/aliyun-sms or tencent/tencent-sms in production');
|
||||
}
|
||||
if (isUnsafeSecret(nextConfig.authCodePepper, DEFAULT_AUTH_CODE_PEPPER)) {
|
||||
failures.push('AUTH_CODE_PEPPER must be a strong production secret');
|
||||
}
|
||||
@@ -100,6 +127,8 @@ function validateProductionConfig(nextConfig: ApiConfig) {
|
||||
}
|
||||
if (nextConfig.storageDefaultProvider === 'local_dev') {
|
||||
failures.push('STORAGE_DEFAULT_PROVIDER=local_dev is not allowed in production');
|
||||
} else if (!PRODUCTION_STORAGE_PROVIDERS.has(nextConfig.storageDefaultProvider)) {
|
||||
failures.push('STORAGE_DEFAULT_PROVIDER must be aliyun_oss, tencent_cos or supabase_storage in production');
|
||||
}
|
||||
if (!nextConfig.storageDefaultBucket.trim()) {
|
||||
failures.push('STORAGE_DEFAULT_BUCKET is required in production');
|
||||
@@ -107,13 +136,22 @@ function validateProductionConfig(nextConfig: ApiConfig) {
|
||||
if (!nextConfig.storageRequireTenantPrefix) {
|
||||
failures.push('STORAGE_REQUIRE_TENANT_PREFIX=false is not allowed in production');
|
||||
}
|
||||
if (nextConfig.storagePublicBaseUrl && !isHttpsProductionUrl(nextConfig.storagePublicBaseUrl)) {
|
||||
failures.push('STORAGE_PUBLIC_BASE_URL must be a production HTTPS URL when configured');
|
||||
}
|
||||
if (nextConfig.storageDefaultProvider === 'aliyun_oss') {
|
||||
if (nextConfig.aliyunOssInternal) {
|
||||
failures.push('ALIYUN_OSS_INTERNAL=true is not allowed for user-facing production signed URLs');
|
||||
}
|
||||
if (!nextConfig.aliyunOssAccessKeyId || !nextConfig.aliyunOssAccessKeySecret) {
|
||||
failures.push('ALIYUN_OSS_ACCESS_KEY_ID and ALIYUN_OSS_ACCESS_KEY_SECRET are required for aliyun_oss');
|
||||
}
|
||||
if (!nextConfig.aliyunOssRegion && !nextConfig.aliyunOssEndpoint) {
|
||||
failures.push('ALIYUN_OSS_REGION or ALIYUN_OSS_ENDPOINT is required for aliyun_oss');
|
||||
}
|
||||
if (nextConfig.aliyunOssEndpoint && (!isHttpsProductionUrl(nextConfig.aliyunOssEndpoint) || !isAllowedHost(nextConfig.aliyunOssEndpoint, ['aliyuncs.com']))) {
|
||||
failures.push('ALIYUN_OSS_ENDPOINT must be a production HTTPS aliyuncs.com endpoint');
|
||||
}
|
||||
}
|
||||
if (nextConfig.storageDefaultProvider === 'tencent_cos') {
|
||||
if (!nextConfig.tencentCosRegion || !nextConfig.tencentCosAppId || !nextConfig.tencentCosSecretId || !nextConfig.tencentCosSecretKey) {
|
||||
@@ -124,6 +162,9 @@ function validateProductionConfig(nextConfig: ApiConfig) {
|
||||
if (!nextConfig.supabaseStorageUrl || !nextConfig.supabaseStorageServiceKey) {
|
||||
failures.push('SUPABASE_STORAGE_URL and SUPABASE_STORAGE_SERVICE_KEY are required for supabase_storage');
|
||||
}
|
||||
if (nextConfig.supabaseStorageUrl && !isHttpsProductionUrl(nextConfig.supabaseStorageUrl)) {
|
||||
failures.push('SUPABASE_STORAGE_URL must be a production HTTPS URL');
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
|
||||
@@ -102,6 +102,25 @@ function isLocalHost(hostname: string) {
|
||||
return ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(hostname);
|
||||
}
|
||||
|
||||
function isHttpsProductionUrl(value: string) {
|
||||
if (!value.trim()) return false;
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === 'https:' && !isLocalHost(url.hostname.toLowerCase());
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isAllowedHost(value: string, allowedHosts: string[]) {
|
||||
try {
|
||||
const hostname = new URL(value).hostname.toLowerCase();
|
||||
return allowedHosts.some(host => hostname === host || hostname.endsWith(`.${host}`));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isUnsafeSecret(value: string) {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return (
|
||||
@@ -121,6 +140,8 @@ function validateProductionConfig(nextConfig: WorkerConfig) {
|
||||
const failures: string[] = [];
|
||||
if (nextConfig.storageDefaultProvider === 'local_dev') {
|
||||
failures.push('STORAGE_DEFAULT_PROVIDER=local_dev is not allowed in production workers');
|
||||
} else if (!['aliyun_oss', 'tencent_cos', 'supabase_storage'].includes(nextConfig.storageDefaultProvider)) {
|
||||
failures.push('STORAGE_DEFAULT_PROVIDER must be aliyun_oss, tencent_cos or supabase_storage in production workers');
|
||||
}
|
||||
if (!nextConfig.storageDefaultBucket.trim()) {
|
||||
failures.push('STORAGE_DEFAULT_BUCKET is required in production workers');
|
||||
@@ -128,6 +149,9 @@ function validateProductionConfig(nextConfig: WorkerConfig) {
|
||||
if (!nextConfig.storageRequireTenantPrefix) {
|
||||
failures.push('STORAGE_REQUIRE_TENANT_PREFIX=false is not allowed in production workers');
|
||||
}
|
||||
if (nextConfig.storagePublicBaseUrl && !isHttpsProductionUrl(nextConfig.storagePublicBaseUrl)) {
|
||||
failures.push('STORAGE_PUBLIC_BASE_URL must be a production HTTPS URL when configured');
|
||||
}
|
||||
if (nextConfig.assetSecurityScanFailOpen) {
|
||||
failures.push('WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=true is not allowed in production workers');
|
||||
}
|
||||
@@ -158,12 +182,18 @@ function validateProductionConfig(nextConfig: WorkerConfig) {
|
||||
}
|
||||
}
|
||||
if (nextConfig.storageDefaultProvider === 'aliyun_oss') {
|
||||
if (nextConfig.aliyunOssInternal) {
|
||||
failures.push('ALIYUN_OSS_INTERNAL=true is not allowed for user-facing production signed URLs');
|
||||
}
|
||||
if (!nextConfig.aliyunOssAccessKeyId || !nextConfig.aliyunOssAccessKeySecret) {
|
||||
failures.push('ALIYUN_OSS_ACCESS_KEY_ID and ALIYUN_OSS_ACCESS_KEY_SECRET are required for aliyun_oss');
|
||||
}
|
||||
if (!nextConfig.aliyunOssRegion && !nextConfig.aliyunOssEndpoint) {
|
||||
failures.push('ALIYUN_OSS_REGION or ALIYUN_OSS_ENDPOINT is required for aliyun_oss');
|
||||
}
|
||||
if (nextConfig.aliyunOssEndpoint && (!isHttpsProductionUrl(nextConfig.aliyunOssEndpoint) || !isAllowedHost(nextConfig.aliyunOssEndpoint, ['aliyuncs.com']))) {
|
||||
failures.push('ALIYUN_OSS_ENDPOINT must be a production HTTPS aliyuncs.com endpoint');
|
||||
}
|
||||
}
|
||||
if (nextConfig.storageDefaultProvider === 'tencent_cos') {
|
||||
if (!nextConfig.tencentCosRegion || !nextConfig.tencentCosAppId || !nextConfig.tencentCosSecretId || !nextConfig.tencentCosSecretKey) {
|
||||
@@ -174,6 +204,9 @@ function validateProductionConfig(nextConfig: WorkerConfig) {
|
||||
if (!nextConfig.supabaseStorageUrl || !nextConfig.supabaseStorageServiceKey) {
|
||||
failures.push('SUPABASE_STORAGE_URL and SUPABASE_STORAGE_SERVICE_KEY are required for supabase_storage');
|
||||
}
|
||||
if (nextConfig.supabaseStorageUrl && !isHttpsProductionUrl(nextConfig.supabaseStorageUrl)) {
|
||||
failures.push('SUPABASE_STORAGE_URL must be a production HTTPS URL');
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user