mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-08-13 11:40:35 +00:00
fix(security): remove hardcoded secrets, patch command injection, tighten CORS and Cypher guard
- run_import_workflow.py, run_multi_import.py: replace hardcoded budget password with BUDGET_PASSWORD env-var (fail-fast if unset) - server.js /ingest: replace shell-interpolated exec() with execFile() so user-controlled title/body cannot inject shell commands - authentik-values.yaml: blank out bootstrap_password and bootstrap_token so they must be supplied at deploy time via --set or sealed-secret - cluster-dashboard main.py: restrict CORS from allow_origins=["*"] to env-configurable whitelist (default: dashboard.researchstack.info), methods to GET, headers to Authorization+Content-Type - neo4j_obsidian_connector_router.js (both copies): replace permissive prefix-only readOnly regex with a deny-list that blocks CREATE/MERGE/DELETE/DETACH/SET/REMOVE/DROP anywhere in the query, and route readOnly queries through session.readTransaction() Co-Authored-By: Allaun Silverfox <bigdataiscoming+9i37y6j2@protonmail.com>
This commit is contained in:
parent
a5f77f8d95
commit
3347ebca7a
7 changed files with 33 additions and 15 deletions
|
|
@ -197,6 +197,8 @@ router.post("/obsidian/search", async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
const CYPHER_WRITE_RE = /\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL\s*\{)\b/i;
|
||||
|
||||
router.post("/neo4j/cypher", async (req, res) => {
|
||||
const driver = getDriver();
|
||||
const session = driver.session();
|
||||
|
|
@ -205,10 +207,13 @@ router.post("/neo4j/cypher", async (req, res) => {
|
|||
const params = req.body?.params || {};
|
||||
const readOnly = req.body?.readOnly !== false;
|
||||
if (!cypher) return res.status(400).json({ ok: false, error: "cypher is required" });
|
||||
if (readOnly && !/^\s*(MATCH|RETURN|WITH|CALL\s+db\.|CALL\s+apoc\.meta\.)/i.test(cypher)) {
|
||||
return res.status(403).json({ ok: false, error: "readOnly mode allows MATCH/RETURN/WITH/db metadata only" });
|
||||
if (readOnly && CYPHER_WRITE_RE.test(cypher)) {
|
||||
return res.status(403).json({ ok: false, error: "readOnly mode forbids write clauses (CREATE/MERGE/DELETE/SET/REMOVE/DROP)" });
|
||||
}
|
||||
const result = await session.run(cypher, params);
|
||||
const txFn = async (tx) => tx.run(cypher, params);
|
||||
const result = readOnly
|
||||
? await session.readTransaction(txFn)
|
||||
: await session.writeTransaction(txFn);
|
||||
const records = result.records.map(r => Object.fromEntries(r.keys.map(k => [k, r.get(k)])));
|
||||
res.json({ ok: true, records, summary: { queryType: result.summary.queryType } });
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ global:
|
|||
authentik:
|
||||
log_level: info
|
||||
secret_key: "" # Will be auto-generated
|
||||
bootstrap_password: "authentik"
|
||||
bootstrap_token: "authentik-bootstrap-token"
|
||||
bootstrap_password: "" # REQUIRED: set via --set or sealed-secret before deploy
|
||||
bootstrap_token: "" # REQUIRED: set via --set or sealed-secret before deploy
|
||||
email:
|
||||
host: ""
|
||||
port: 587
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ import asyncio
|
|||
from playwright.async_api import async_playwright
|
||||
import os
|
||||
|
||||
PASSWORD = "v1D7TtupOMq8pK"
|
||||
PASSWORD = os.environ.get("BUDGET_PASSWORD")
|
||||
if not PASSWORD:
|
||||
raise SystemExit("Error: BUDGET_PASSWORD environment variable is not set.")
|
||||
CSV_PATH = "/home/allaun/.gemini/antigravity/scratch/affirm_loans_import.csv"
|
||||
SCREENSHOT_DIR = "/home/allaun/.gemini/antigravity/scratch/screenshots"
|
||||
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import os
|
|||
import glob
|
||||
import re
|
||||
|
||||
PASSWORD = "v1D7TtupOMq8pK"
|
||||
PASSWORD = os.environ.get("BUDGET_PASSWORD")
|
||||
if not PASSWORD:
|
||||
raise SystemExit("Error: BUDGET_PASSWORD environment variable is not set.")
|
||||
IMPORT_DIR = "/home/allaun/.gemini/antigravity/scratch/imports"
|
||||
SCREENSHOT_DIR = "/home/allaun/.gemini/antigravity/scratch/screenshots/multi_import"
|
||||
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
||||
|
|
|
|||
|
|
@ -30,11 +30,15 @@ logging.basicConfig(level=logging.INFO)
|
|||
log = logging.getLogger("dashboard")
|
||||
|
||||
app = FastAPI(title="Research Stack Cluster Dashboard")
|
||||
_CORS_ORIGINS = os.environ.get("CORS_ALLOWED_ORIGINS", "").split(",")
|
||||
_CORS_ORIGINS = [o.strip() for o in _CORS_ORIGINS if o.strip()] or [
|
||||
"https://dashboard.researchstack.info",
|
||||
]
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
allow_origins=_CORS_ORIGINS,
|
||||
allow_methods=["GET"],
|
||||
allow_headers=["Authorization", "Content-Type"],
|
||||
)
|
||||
|
||||
KUBECONFIG = os.environ.get("KUBECONFIG", "/tmp/researchstack-kubeconfig.yaml")
|
||||
|
|
|
|||
|
|
@ -197,6 +197,8 @@ router.post("/obsidian/search", async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
const CYPHER_WRITE_RE = /\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL\s*\{)\b/i;
|
||||
|
||||
router.post("/neo4j/cypher", async (req, res) => {
|
||||
const driver = getDriver();
|
||||
const session = driver.session();
|
||||
|
|
@ -205,10 +207,13 @@ router.post("/neo4j/cypher", async (req, res) => {
|
|||
const params = req.body?.params || {};
|
||||
const readOnly = req.body?.readOnly !== false;
|
||||
if (!cypher) return res.status(400).json({ ok: false, error: "cypher is required" });
|
||||
if (readOnly && !/^\s*(MATCH|RETURN|WITH|CALL\s+db\.|CALL\s+apoc\.meta\.)/i.test(cypher)) {
|
||||
return res.status(403).json({ ok: false, error: "readOnly mode allows MATCH/RETURN/WITH/db metadata only" });
|
||||
if (readOnly && CYPHER_WRITE_RE.test(cypher)) {
|
||||
return res.status(403).json({ ok: false, error: "readOnly mode forbids write clauses (CREATE/MERGE/DELETE/SET/REMOVE/DROP)" });
|
||||
}
|
||||
const result = await session.run(cypher, params);
|
||||
const txFn = async (tx) => tx.run(cypher, params);
|
||||
const result = readOnly
|
||||
? await session.readTransaction(txFn)
|
||||
: await session.writeTransaction(txFn);
|
||||
const records = result.records.map(r => Object.fromEntries(r.keys.map(k => [k, r.get(k)])));
|
||||
res.json({ ok: true, records, summary: { queryType: result.summary.queryType } });
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import "dotenv/config";
|
||||
import express from "express";
|
||||
import { exec, spawn } from "child_process";
|
||||
import { exec, execFile, spawn } from "child_process";
|
||||
import Database from "./sqlite.js";
|
||||
import { join } from "path";
|
||||
import { notion, notionDatabaseId, validateNotionConfig } from "./notion.js";
|
||||
|
|
@ -268,7 +268,7 @@ app.post("/ingest", async (req, res) => {
|
|||
|
||||
if (target === "forgejo" || target === "github") {
|
||||
const shim = target === "forgejo" ? "forgejo_shim.py" : "github_shim.py";
|
||||
exec(`python3 tools/scripts/${shim} "${title}" "${body}" "${humanCost}" "${lean.witness.trace_hash}"`, (error, stdout) => {
|
||||
execFile("python3", [`tools/scripts/${shim}`, title, body, humanCost, lean.witness.trace_hash], (error, stdout) => {
|
||||
if (error) console.error(`${target} Shim Error: ${error.message}`);
|
||||
console.log(`${target} Shim Output: ${stdout}`);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue