mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-08-20 13:17:28 +00:00
fix(security): add CALL procedure allowlist for Cypher readOnly, add OPTIONS to CORS
- Cypher guard: restore positive allowlist for CALL targets (only db.* and apoc.meta.* allowed in readOnly mode). Extract cypherReadOnlyViolation() helper for clarity. Both copies updated. - CORS: add OPTIONS to allow_methods so preflight requests succeed. Co-Authored-By: Allaun Silverfox <bigdataiscoming+9i37y6j2@protonmail.com>
This commit is contained in:
parent
3347ebca7a
commit
3555cbb26c
3 changed files with 23 additions and 5 deletions
|
|
@ -198,6 +198,14 @@ router.post("/obsidian/search", async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const CYPHER_WRITE_RE = /\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL\s*\{)\b/i;
|
const CYPHER_WRITE_RE = /\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL\s*\{)\b/i;
|
||||||
|
const CYPHER_CALL_RE = /\bCALL\b/i;
|
||||||
|
const CYPHER_CALL_ALLOWED_RE = /\bCALL\s+(db\.|apoc\.meta\.)/i;
|
||||||
|
|
||||||
|
function cypherReadOnlyViolation(cypher) {
|
||||||
|
if (CYPHER_WRITE_RE.test(cypher)) return "readOnly mode forbids write clauses (CREATE/MERGE/DELETE/SET/REMOVE/DROP)";
|
||||||
|
if (CYPHER_CALL_RE.test(cypher) && !CYPHER_CALL_ALLOWED_RE.test(cypher)) return "readOnly mode only allows CALL db.* and CALL apoc.meta.*";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
router.post("/neo4j/cypher", async (req, res) => {
|
router.post("/neo4j/cypher", async (req, res) => {
|
||||||
const driver = getDriver();
|
const driver = getDriver();
|
||||||
|
|
@ -207,8 +215,9 @@ router.post("/neo4j/cypher", async (req, res) => {
|
||||||
const params = req.body?.params || {};
|
const params = req.body?.params || {};
|
||||||
const readOnly = req.body?.readOnly !== false;
|
const readOnly = req.body?.readOnly !== false;
|
||||||
if (!cypher) return res.status(400).json({ ok: false, error: "cypher is required" });
|
if (!cypher) return res.status(400).json({ ok: false, error: "cypher is required" });
|
||||||
if (readOnly && CYPHER_WRITE_RE.test(cypher)) {
|
const violation = readOnly ? cypherReadOnlyViolation(cypher) : null;
|
||||||
return res.status(403).json({ ok: false, error: "readOnly mode forbids write clauses (CREATE/MERGE/DELETE/SET/REMOVE/DROP)" });
|
if (violation) {
|
||||||
|
return res.status(403).json({ ok: false, error: violation });
|
||||||
}
|
}
|
||||||
const txFn = async (tx) => tx.run(cypher, params);
|
const txFn = async (tx) => tx.run(cypher, params);
|
||||||
const result = readOnly
|
const result = readOnly
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ _CORS_ORIGINS = [o.strip() for o in _CORS_ORIGINS if o.strip()] or [
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=_CORS_ORIGINS,
|
allow_origins=_CORS_ORIGINS,
|
||||||
allow_methods=["GET"],
|
allow_methods=["GET", "OPTIONS"],
|
||||||
allow_headers=["Authorization", "Content-Type"],
|
allow_headers=["Authorization", "Content-Type"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,14 @@ router.post("/obsidian/search", async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const CYPHER_WRITE_RE = /\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL\s*\{)\b/i;
|
const CYPHER_WRITE_RE = /\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL\s*\{)\b/i;
|
||||||
|
const CYPHER_CALL_RE = /\bCALL\b/i;
|
||||||
|
const CYPHER_CALL_ALLOWED_RE = /\bCALL\s+(db\.|apoc\.meta\.)/i;
|
||||||
|
|
||||||
|
function cypherReadOnlyViolation(cypher) {
|
||||||
|
if (CYPHER_WRITE_RE.test(cypher)) return "readOnly mode forbids write clauses (CREATE/MERGE/DELETE/SET/REMOVE/DROP)";
|
||||||
|
if (CYPHER_CALL_RE.test(cypher) && !CYPHER_CALL_ALLOWED_RE.test(cypher)) return "readOnly mode only allows CALL db.* and CALL apoc.meta.*";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
router.post("/neo4j/cypher", async (req, res) => {
|
router.post("/neo4j/cypher", async (req, res) => {
|
||||||
const driver = getDriver();
|
const driver = getDriver();
|
||||||
|
|
@ -207,8 +215,9 @@ router.post("/neo4j/cypher", async (req, res) => {
|
||||||
const params = req.body?.params || {};
|
const params = req.body?.params || {};
|
||||||
const readOnly = req.body?.readOnly !== false;
|
const readOnly = req.body?.readOnly !== false;
|
||||||
if (!cypher) return res.status(400).json({ ok: false, error: "cypher is required" });
|
if (!cypher) return res.status(400).json({ ok: false, error: "cypher is required" });
|
||||||
if (readOnly && CYPHER_WRITE_RE.test(cypher)) {
|
const violation = readOnly ? cypherReadOnlyViolation(cypher) : null;
|
||||||
return res.status(403).json({ ok: false, error: "readOnly mode forbids write clauses (CREATE/MERGE/DELETE/SET/REMOVE/DROP)" });
|
if (violation) {
|
||||||
|
return res.status(403).json({ ok: false, error: violation });
|
||||||
}
|
}
|
||||||
const txFn = async (tx) => tx.run(cypher, params);
|
const txFn = async (tx) => tx.run(cypher, params);
|
||||||
const result = readOnly
|
const result = readOnly
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue