refactor: comprehensive code review fixes and security hardening

Security:
- Add CSP headers for HTML reports (style-src 'unsafe-inline')
- Restrict origin validation to specific .kappa-d8e.workers.dev domain
- Add base64 size limit (100KB) for report data parameter
- Implement rejection sampling for unbiased password generation
- Add SQL LIKE pattern escaping for tech specs query
- Add security warning for plaintext password storage (TODO: encrypt)

Performance:
- Add Telegram API timeout (10s) with AbortController
- Fix rate limiter sorting by resetTime for proper cleanup
- Use centralized TIMEOUTS config for VPS provider APIs

Features:
- Add admin SSH key support for server recovery access
  - ADMIN_SSH_PUBLIC_KEY for Linode (public key string)
  - ADMIN_SSH_KEY_ID_VULTR for Vultr (pre-registered key ID)
- Add origin validation middleware
- Add idempotency key migration

Code Quality:
- Return 404 status when no servers found
- Consolidate error logging to single JSON.stringify call
- Import TECH_CATEGORY_WEIGHTS from config.ts
- Add escapeLikePattern utility function

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-29 11:36:08 +09:00
parent d41f1ee841
commit 5319bf3e4c
27 changed files with 965 additions and 530 deletions

View File

@@ -87,20 +87,22 @@ export class ProvisioningRepository {
async createServerOrder(
userId: number,
telegramUserId: string,
specId: number,
region: string,
pricePaid: number,
label: string | null,
image: string | null
image: string | null,
idempotencyKey: string | null
): Promise<ServerOrder> {
const result = await this.userDb
.prepare(
`INSERT INTO server_orders
(user_id, spec_id, status, region, price_paid, label, image, billing_type)
VALUES (?, ?, 'pending', ?, ?, ?, ?, 'monthly')
(user_id, telegram_user_id, spec_id, status, region, price_paid, label, image, billing_type, idempotency_key, created_at, expires_at)
VALUES (?, ?, ?, 'pending', ?, ?, ?, ?, 'monthly', ?, CURRENT_TIMESTAMP, datetime(CURRENT_TIMESTAMP, '+720 hours'))
RETURNING *`
)
.bind(userId, specId, region, pricePaid, label, image)
.bind(userId, telegramUserId, specId, region, pricePaid, label, image, idempotencyKey)
.first();
return result as unknown as ServerOrder;
@@ -132,7 +134,16 @@ export class ProvisioningRepository {
}
}
/**
* Update order with root password
*
* SECURITY WARNING: Password is currently stored in plaintext.
* TODO: Implement encryption using WebCrypto API (AES-GCM) before production use.
* The encryption key should be stored in env.ENCRYPTION_KEY secret.
*/
async updateOrderRootPassword(orderId: number, rootPassword: string): Promise<void> {
// TODO: Encrypt password before storage
// const encryptedPassword = await this.encryptPassword(rootPassword, env.ENCRYPTION_KEY);
await this.userDb
.prepare(
`UPDATE server_orders
@@ -176,7 +187,9 @@ export class ProvisioningRepository {
async getOrdersByUserId(userId: number, limit: number = 20): Promise<ServerOrder[]> {
const result = await this.userDb
.prepare(
'SELECT * FROM server_orders WHERE user_id = ? ORDER BY created_at DESC LIMIT ?'
`SELECT * FROM server_orders
WHERE user_id = ? AND status NOT IN ('terminated', 'cancelled')
ORDER BY created_at DESC LIMIT ?`
)
.bind(userId, limit)
.all();
@@ -184,6 +197,18 @@ export class ProvisioningRepository {
return result.results as unknown as ServerOrder[];
}
/**
* Find order by idempotency key (for duplicate prevention)
*/
async findOrderByIdempotencyKey(idempotencyKey: string): Promise<ServerOrder | null> {
const result = await this.userDb
.prepare('SELECT * FROM server_orders WHERE idempotency_key = ?')
.bind(idempotencyKey)
.first();
return result as unknown as ServerOrder | null;
}
// ============================================
// Pricing Lookup (cloud-instances-db)
// Uses anvil_pricing, anvil_instances, anvil_regions
@@ -233,11 +258,13 @@ export class ProvisioningRepository {
*/
async createOrderWithPayment(
userId: number,
telegramUserId: string,
specId: number,
region: string,
priceKrw: number,
label: string | null,
image: string | null
image: string | null,
idempotencyKey: string | null
): Promise<{ orderId: number | null; error?: string }> {
try {
// Step 1: Check and deduct balance
@@ -249,11 +276,13 @@ export class ProvisioningRepository {
// Step 2: Create order
const order = await this.createServerOrder(
userId,
telegramUserId,
specId,
region,
priceKrw,
label,
image
image,
idempotencyKey
);
return { orderId: order.id };