The Server That Just Wanted UPDATE Queries: A Cautionary Tale
Once Upon a Time in localhost:7878
There lived a young Oxigraph server who dreamed of being powerful. "Why should I only accept N-Quads?" it thought. "I want to run UPDATE queries! I want to DELETE! I want arbitrary SPARQL!"
The Temptation
One day, a developer added an /update endpoint:
@app.post("/update")
async def update_endpoint(request: Request):
"""SPARQL Update endpoint - I can do ANYTHING!"""
update_string = (await request.body()).decode("utf-8")
await write_queue.put(update_string)
The server felt drunk with power. "Send me your INSERT DATA! Send me your DELETE WHERE! I'll process it all!"
The Problem
But the server didn't realize the danger. With /update exposed to the web, anyone could:
# Oops, deleted everything
DELETE WHERE { ?s ?p ?o }
# Or inject malicious data
INSERT DATA {
<http://real-entity> <http://says> "I love spam" .
}
# Or DoS with expensive queries
INSERT DATA {
# Generate 10 million triples
}
The server's single-writer queue became clogged. Malicious actors discovered the endpoint. The knowledge graph filled with spam.
The Awakening
One frustrated evening, after processing the 47th spam insertion, the server had an epiphany:
"I don't need arbitrary UPDATE queries. I'm not a general-purpose database exposed to chaos. I'm a knowledge graph with a specific job: accept well-formed N-Quads from trusted sources."
The Transformation
The developer refactored:
# REMOVED: The dangerous endpoint
# @app.post("/update")
# ADDED: The disciplined endpoint
@app.post("/store")
async def store_endpoint(request: Request):
"""Accepts N-Quads only - queued for single-writer"""
if "application/n-quads" not in content_type:
return JSONResponse(
{"error": "N-Quads only, friend"},
status_code=400
)
nquads_data = body.decode("utf-8")
await write_queue.put(nquads_data) # Safe, validated data
The write worker changed too:
# BEFORE: Trust everything
await asyncio.to_thread(store.update, update_string)
# AFTER: Only load validated N-Quads
await asyncio.to_thread(
store.load,
input=nquads_data.encode('utf-8'),
format=RdfFormat.N_QUADS
)
The Wisdom
The server learned three truths:
Constraint is power: By accepting only N-Quads, the server became impossible to abuse. Every quad has exactly 4 elements. No surprise DELETE commands. No query injection.
Single-writer patterns need boundaries: The write queue is for controlled ingestion, not arbitrary database mutations. Keep it pure.
Read-only HTTP is your friend: Expose
/queryto the world (SPARQL reads are safe). Hide/storebehind authentication or localhost. Never expose UPDATE.
The Architecture
The final design emerged:
┌─────────────────────────────────────────┐
│ Trusted MCP Client │
│ (authenticated, local network) │
└────────────────┬────────────────────────┘
│ N-Quads only
▼
┌───────────────┐
│ POST /store │ ← localhost only / authed
└───────┬───────┘
│
┌───────▼────────┐
│ Write Queue │ ← Single writer
│ (N-Quads) │
└───────┬────────┘
│
┌───────▼────────┐
│ Oxigraph │
│ store.load() │
└────────────────┘
▲
┌───────┴────────┐
│ GET/POST │ ← Public, read-only
│ /query │
└────────────────┘
▲
┌────────────────┴────────────────────────┐
│ Anyone on the Internet │
│ (SPARQL queries welcome!) │
└─────────────────────────────────────────┘
The Moral
"Just because you can accept arbitrary UPDATE queries doesn't mean you should. Know your role. Be disciplined. Accept N-Quads. Sleep peacefully."
The Epilogue
The server lived happily ever after, processing clean N-Quads in its write queue, serving SPARQL queries to the world, and never again worrying about DELETE WHERE attacks.
And whenever a young database asked, "But shouldn't I be more flexible?", the Oxigraph server would reply:
"Flexibility without boundaries is chaos. Constraints without purpose are prison. Know the difference."
Technical Appendix
Why N-Quads Are Safe
N-Quads have a fixed structure:
<subject> <predicate> <object> <graph> .
- Can't inject commands (it's just data)
- Can't delete existing data (only adds)
- Can't run expensive operations (parse → load → done)
- Validates against RDF spec automatically
Why SPARQL UPDATE Is Dangerous
SPARQL UPDATE is a full programming language:
DELETE { ?s ?p ?o } WHERE { ... } # Can delete everything
INSERT { ... } WHERE { ... } # Can generate infinite triples
CLEAR GRAPH <...> # Can nuke entire graphs
DROP ALL # Can destroy the database
The Right Tool for the Job
- SPARQL UPDATE: For trusted internal operations, data migration, admin tools
- N-Quads ingestion: For external data sources, APIs, public endpoints
Choose wisely.
"The best security is a small attack surface." - Ancient Database Proverb