v0.1.34 — security: command injection, version/health auth, SSRF
This commit is contained in:
parent
71915dba04
commit
99b00674fd
6 changed files with 45 additions and 9 deletions
|
|
@ -5,6 +5,24 @@ import { getSetting, setSetting } from "@/lib/db";
|
|||
|
||||
const PUBLIC_KEYS = ["base_domain", "admin_email", "update_auto", "update_include_prereleases", "hits_retention_days", "webhook_url"];
|
||||
|
||||
function isPrivateUrl(raw: string): boolean {
|
||||
try {
|
||||
const { hostname } = new URL(raw);
|
||||
const h = hostname.replace(/^\[|\]$/g, ""); // strip IPv6 brackets
|
||||
if (/^localhost$/i.test(h)) return true;
|
||||
if (/^127\./.test(h)) return true;
|
||||
if (/^10\./.test(h)) return true;
|
||||
if (/^192\.168\./.test(h)) return true;
|
||||
if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
|
||||
if (/^169\.254\./.test(h)) return true;
|
||||
if (/^::1$/.test(h)) return true;
|
||||
if (/^fc[0-9a-f]{2}/i.test(h)) return true;
|
||||
return false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
|
||||
|
|
@ -20,6 +38,9 @@ export async function PATCH(req: Request) {
|
|||
const body = await req.json().catch(() => ({}));
|
||||
for (const [k, v] of Object.entries(body)) {
|
||||
if (!PUBLIC_KEYS.includes(k)) continue;
|
||||
if (k === "webhook_url" && v && isPrivateUrl(String(v))) {
|
||||
return NextResponse.json({ error: "webhook_url must not point to a private or loopback address" }, { status: 422 });
|
||||
}
|
||||
setSetting(k, String(v));
|
||||
}
|
||||
return NextResponse.json({ ok: true });
|
||||
|
|
|
|||
|
|
@ -2,9 +2,16 @@ import { NextResponse } from "next/server";
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { getDb } from "@/lib/db";
|
||||
import { authenticateToken } from "@/lib/api-auth";
|
||||
import pkg from "../../../../package.json";
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(req: Request) {
|
||||
const authed = authenticateToken(req);
|
||||
|
||||
if (!authed) {
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const since24h = Date.now() - 24 * 60 * 60 * 1000;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { requireScope } from "@/lib/api-auth";
|
||||
import { getUpdateStatus } from "@/lib/updater";
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(req: Request) {
|
||||
const auth = requireScope(req, "read:domains");
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
return NextResponse.json(getUpdateStatus());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { exec } from "child_process";
|
||||
import { execFile } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getDb, getSetting, type DomainRow, type DomainGroupRow } from "./db";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const CADDYFILE_PATH = process.env.NEXREDIRECT_CADDYFILE || "/etc/caddy/Caddyfile";
|
||||
const CADDY_ADMIN = process.env.CADDY_ADMIN_URL || "http://localhost:2019";
|
||||
|
|
@ -84,7 +84,7 @@ export async function reloadCaddy(): Promise<{ ok: boolean; error?: string }> {
|
|||
|
||||
// Try shell `caddy reload` first — it talks to admin API as caddy itself, no Origin-header issues.
|
||||
try {
|
||||
await execAsync(`caddy reload --config ${CADDYFILE_PATH} --address localhost:2019`, { timeout: 30_000 });
|
||||
await execFileAsync("caddy", ["reload", "--config", CADDYFILE_PATH, "--address", "localhost:2019"], { timeout: 30_000 });
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
// Fall back to direct admin API POST (older Caddy / different admin URL).
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { exec } from "child_process";
|
||||
import { execFile } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getSetting, setSetting, getDb } from "./db";
|
||||
import pkg from "../package.json";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const VALID_TAG = /^v?\d+\.\d+\.\d+(-[\w.]+)?$/;
|
||||
|
||||
const REPO = process.env.NEXREDIRECT_REPO || "CoreXManagement/CoreX-NexRedirect";
|
||||
|
||||
|
|
@ -104,11 +106,14 @@ export async function applyUpdate(): Promise<{ ok: boolean; from: string; to: st
|
|||
if (!to || !status.update_available) {
|
||||
return { ok: false, from, to, error: "no_update" };
|
||||
}
|
||||
if (!VALID_TAG.test(to)) {
|
||||
return { ok: false, from, to, error: "invalid_tag" };
|
||||
}
|
||||
|
||||
const updateScript = process.env.NEXREDIRECT_UPDATE_SCRIPT || "/opt/corex-nexredirect/scripts/update.sh";
|
||||
const start = Date.now();
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(`sudo -n ${updateScript} ${to}`, { timeout: 5 * 60 * 1000 });
|
||||
const { stdout, stderr } = await execFileAsync("sudo", ["-n", updateScript, to], { timeout: 5 * 60 * 1000 });
|
||||
getDb().prepare("INSERT INTO update_log (from_version, to_version, ts, status, log) VALUES (?, ?, ?, 'success', ?)")
|
||||
.run(from, to, start, (stdout + "\n" + stderr).slice(0, 10000));
|
||||
return { ok: true, from, to };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "corex-nexredirect",
|
||||
"version": "0.1.33",
|
||||
"version": "0.1.34",
|
||||
"license": "MIT",
|
||||
"overrides": {
|
||||
"postcss": "^8.5.13",
|
||||
|
|
|
|||
Loading…
Reference in a new issue