mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-08-13 08:00:35 +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_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) => {
|
||||
const driver = getDriver();
|
||||
|
|
@ -207,8 +215,9 @@ 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 && 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 violation = readOnly ? cypherReadOnlyViolation(cypher) : null;
|
||||
if (violation) {
|
||||
return res.status(403).json({ ok: false, error: violation });
|
||||
}
|
||||
const txFn = async (tx) => tx.run(cypher, params);
|
||||
const result = readOnly
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ _CORS_ORIGINS = [o.strip() for o in _CORS_ORIGINS if o.strip()] or [
|
|||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=_CORS_ORIGINS,
|
||||
allow_methods=["GET"],
|
||||
allow_methods=["GET", "OPTIONS"],
|
||||
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_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) => {
|
||||
const driver = getDriver();
|
||||
|
|
@ -207,8 +215,9 @@ 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 && 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 violation = readOnly ? cypherReadOnlyViolation(cypher) : null;
|
||||
if (violation) {
|
||||
return res.status(403).json({ ok: false, error: violation });
|
||||
}
|
||||
const txFn = async (tx) => tx.run(cypher, params);
|
||||
const result = readOnly
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue