mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-04-12 22:18:22 +03:00
fix: resolve CVEs, upgrade to Astro v6, and harden API security (#227)
* fix: resolve CVEs, upgrade to Astro v6, and harden API security
Docker image CVE fixes:
- Install git-lfs v3.7.1 from GitHub releases (Go 1.25) instead of
Debian apt (Go 1.23.12), fixing CVE-2025-68121 and 8 other Go stdlib CVEs
- Strip build-only packages (esbuild, vite, rollup, svgo, tailwindcss)
from production image, eliminating 9 esbuild Go stdlib CVEs
Dependency upgrades:
- Astro v5 → v6 (includes Vite 7, Zod 4)
- Remove legacy content config (src/content/config.ts)
- Update HealthResponse type for simplified health endpoint
- npm overrides for fast-xml-parser ≥5.3.6, devalue ≥5.6.2,
node-forge ≥1.3.2, svgo ≥4.0.1, rollup ≥4.59.0
API security hardening:
- /api/auth/debug: dev-only, require auth, remove user-creation POST,
strip trustedOrigins/databaseConfig from response
- /api/auth/check-users: return boolean hasUsers instead of exact count
- /api/cleanup/auto: require authentication, remove per-user details
- /api/health: remove OS version, memory, uptime from response
- /api/config: validate Gitea URL protocol (http/https only)
- BETTER_AUTH_SECRET: log security warning when using insecure defaults
- generateRandomString: replace Math.random() with crypto.getRandomValues()
- hashValue: add random salt and timing-safe verification
* repositories: migrate table to tanstack
* Revert "repositories: migrate table to tanstack"
This reverts commit a544b29e6d.
* fixed lock file
This commit is contained in:
@@ -91,35 +91,17 @@ export const giteaApi = {
|
||||
|
||||
// Health API
|
||||
export interface HealthResponse {
|
||||
status: "ok" | "error";
|
||||
status: "ok" | "error" | "degraded";
|
||||
timestamp: string;
|
||||
version: string;
|
||||
latestVersion: string;
|
||||
updateAvailable: boolean;
|
||||
database: {
|
||||
connected: boolean;
|
||||
message: string;
|
||||
};
|
||||
system: {
|
||||
uptime: {
|
||||
startTime: string;
|
||||
uptimeMs: number;
|
||||
formatted: string;
|
||||
};
|
||||
memory: {
|
||||
rss: string;
|
||||
heapTotal: string;
|
||||
heapUsed: string;
|
||||
external: string;
|
||||
systemTotal: string;
|
||||
systemFree: string;
|
||||
};
|
||||
os: {
|
||||
platform: string;
|
||||
version: string;
|
||||
arch: string;
|
||||
};
|
||||
env: string;
|
||||
recovery?: {
|
||||
status: string;
|
||||
jobsNeedingRecovery: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,23 @@ export const ENV = {
|
||||
},
|
||||
|
||||
// Better Auth secret for authentication
|
||||
BETTER_AUTH_SECRET:
|
||||
process.env.BETTER_AUTH_SECRET || "your-secret-key-change-this-in-production",
|
||||
get BETTER_AUTH_SECRET(): string {
|
||||
const secret = process.env.BETTER_AUTH_SECRET;
|
||||
const knownInsecureDefaults = [
|
||||
"your-secret-key-change-this-in-production",
|
||||
"dev-only-insecure-secret-do-not-use-in-production",
|
||||
];
|
||||
if (!secret || knownInsecureDefaults.includes(secret)) {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
console.error(
|
||||
"\x1b[31m[SECURITY WARNING]\x1b[0m BETTER_AUTH_SECRET is missing or using an insecure default. " +
|
||||
"Set a strong secret: openssl rand -base64 32"
|
||||
);
|
||||
}
|
||||
return secret || "dev-only-insecure-secret-do-not-use-in-production";
|
||||
}
|
||||
return secret;
|
||||
},
|
||||
|
||||
// Server host and port
|
||||
HOST: process.env.HOST || "localhost",
|
||||
|
||||
@@ -11,9 +11,11 @@ export function cn(...inputs: ClassValue[]) {
|
||||
|
||||
export function generateRandomString(length: number): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const randomValues = new Uint32Array(length);
|
||||
crypto.getRandomValues(randomValues);
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
result += chars.charAt(randomValues[i] % chars.length);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -160,10 +160,23 @@ export function generateSecureToken(length: number = 32): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes a value using SHA-256 (for non-reversible values like API keys for comparison)
|
||||
* Hashes a value using SHA-256 with a random salt (for non-reversible values like API keys)
|
||||
* @param value The value to hash
|
||||
* @returns Hex encoded hash
|
||||
* @returns Salt and hash in format "salt:hash"
|
||||
*/
|
||||
export function hashValue(value: string): string {
|
||||
return crypto.createHash('sha256').update(value).digest('hex');
|
||||
const salt = crypto.randomBytes(16).toString('hex');
|
||||
const hash = crypto.createHash('sha256').update(salt + value).digest('hex');
|
||||
return `${salt}:${hash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a value against a salted hash produced by hashValue()
|
||||
* Uses constant-time comparison to prevent timing attacks
|
||||
*/
|
||||
export function verifyHash(value: string, saltedHash: string): boolean {
|
||||
const [salt, expectedHash] = saltedHash.split(':');
|
||||
if (!salt || !expectedHash) return false;
|
||||
const actualHash = crypto.createHash('sha256').update(salt + value).digest('hex');
|
||||
return crypto.timingSafeEqual(Buffer.from(actualHash, 'hex'), Buffer.from(expectedHash, 'hex'));
|
||||
}
|
||||
Reference in New Issue
Block a user