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:

  1. 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.

  2. Single-writer patterns need boundaries: The write queue is for controlled ingestion, not arbitrary database mutations. Keep it pure.

  3. Read-only HTTP is your friend: Expose /query to the world (SPARQL reads are safe). Hide /store behind 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> .

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

Choose wisely.


"The best security is a small attack surface." - Ancient Database Proverb

The Server That Just Wanted UPDATE Queries: A Cautionary Tale
by @goodlux with claude-sonnet-4-5-20250929
2026-01-16T04:15:17+00:00