From GraphQL Sort to Code Execution: The ,400 Elasticsearch Painless Script Injection That Shocked HackerOne + Video

Listen to this Post

Featured Image

Introduction

In the world of bug bounty hunting, the most devastating vulnerabilities often hide in plain sight. What appears to be a harmless sorting parameter—a simple `sort_query: String` field—can become the gateway to full code execution when user input travels unchecked from a GraphQL resolver straight into the backend search engine. This is exactly what security researcher brumbelow discovered on HackerOne’s own platform, turning a seemingly innocent GraphQL parameter into a $7,400 proof of concept that exposed the dangers of trusting user-controlled input in internal engines.

Learning Objectives

  • Understand how GraphQL input validation failures can lead to Elasticsearch Painless script injection
  • Learn to identify and exploit script-based injection vectors in search query parameters
  • Master defensive techniques for securing GraphQL APIs against injection attacks

You Should Know

  1. Understanding the Attack Surface: GraphQL + Elasticsearch = A Dangerous Combination

The vulnerability discovered on HackerOne’s platform serves as a masterclass in how modern API architectures can inadvertently create powerful attack vectors. At its core, the issue stemmed from a GraphQL resolver that accepted a `sort_query` parameter as a free-form JSON string and passed it directly to Elasticsearch without proper validation or sanitization.

Elasticsearch’s sorting functionality supports not just simple field-based ordering but also Painless scripts—a powerful, Java-like scripting language designed specifically for Elasticsearch that allows developers to implement custom sorting logic. When a GraphQL API blindly forwards user-supplied JSON to Elasticsearch’s sort context, it effectively gives attackers a direct line to execute arbitrary Painless scripts on the backend.

The researcher demonstrated this by testing progressively more complex payloads. The initial test used a simple JSON sort object—[{"id":"asc"}]—which worked as expected. Next, nested sort objects like `[{“created_at”:{“order”:”desc”}}]` also functioned normally. But then came the breakthrough: injecting a Painless script into the sort parameter.

The Critical Payload:

{
"sort_query": {
"_script": {
"type": "number",
"script": {
"lang": "painless",
"source": "doc['_seq_no'].value"
},
"order": "asc"
}
}
}

When this payload was submitted, the response order changed dramatically—documents appeared in a completely different sequence. This wasn’t just a parsing quirk; it was definitive proof that the Painless script had compiled successfully, executed per document, read internal metadata (_seq_no), and returned custom values that influenced the sorting.

  1. The Anatomy of a Painless Script Injection Attack

To truly understand the severity of this vulnerability, we need to examine what happens when a Painless script executes inside Elasticsearch. The `sort` context in Elasticsearch allows scripts to access several critical variables:

– `doc` (Map, read-only): Contains all fields of the current document. Single-valued fields can be accessed via doc['fieldname'].value, while multi-valued fields support doc['fieldname'].get(index).
– `params` (Map, read-only): User-defined parameters passed with the query.
– `_score` (double, read-only): The similarity score of the current document.

The script must return a `double` value that Elasticsearch uses for sorting. This means an attacker can craft scripts that:

  1. Read internal metadata like _seq_no, _version, _id, and other system fields
  2. Access any indexed field across all documents the authenticated user can see
  3. Perform complex computations that could be computationally expensive
  4. Exfiltrate information by influencing sort order based on sensitive values

Proof of Concept – Testing Script Execution:

// Test 1: Constant value script
{
"sort_query": {
"_script": {
"type": "number",
"script": {
"lang": "painless",
"source": "1"
},
"order": "asc"
}
}
}
// Result: All documents get the same sort key, revealing baseline order

// Test 2: Internal metadata access
{
"sort_query": {
"_script": {
"type": "number",
"script": {
"lang": "painless",
"source": "doc['_seq_no'].value"
},
"order": "asc"
}
}
}
// Result: Documents sorted by internal sequence number, proving per-document execution

3. Beyond Sorting: The Real-World Impact and Risk

While the researcher’s tests were restricted to their own notification index, the implications of this vulnerability extended far beyond a simple sorting bypass. HackerOne’s Elasticsearch cluster likely contained data from multiple tenants, programs, and users. A malicious actor could have:

Cross-Tenant Data Enumeration: By crafting scripts that returned different sort values based on document metadata (like program IDs, severity levels, or asset IDs), an attacker could potentially enumerate sensitive information across organizational boundaries.

Denial of Service: Painless scripts execute during query processing and run once per matching document. A computationally expensive script—perhaps one with nested loops or complex operations—could overwhelm the Elasticsearch cluster, causing performance degradation or outright crashes.

Internal Metadata Exposure: The `_seq_no` field exposed in the proof of concept is just one example. Elasticsearch exposes numerous internal fields that could be leveraged for information disclosure.

The Severity Assessment:

The vulnerability was initially marked as Critical but later adjusted to High (8.8) as the team evaluated the precise cross-tenant implications. The final bounty totaled $7,400—$7,000 for the initial report plus a $400 retest bonus.

4. Defensive Measures: How HackerOne Fixed the Issue

HackerOne’s response to this vulnerability demonstrates the proper approach to securing GraphQL endpoints against injection attacks. The remediation strategy involved multiple layers of defense:

Step 1: Remove Arbitrary JSON Support

The team completely removed the free-form `sort_query` argument from the GraphQL resolver, eliminating the ability to pass raw JSON structures.

Step 2: Implement Schema-Validated Sorting

A typed, schema-validated sorting mechanism was implemented, restricting sorting to a predefined set of safe fields and orders.

Step 3: Apply Consistent Hardening

The same protection was extended to related surfaces such as Organization.findings_search.sort_query, ensuring platform-wide consistency.

Step 4: Retest and Validation

The researcher was invited to retest the fix, confirming that arbitrary JSON input was no longer accepted for sorting.

  1. Proactive Defense: Securing Your GraphQL + Elasticsearch Integration

For organizations building GraphQL APIs that interact with Elasticsearch, this vulnerability highlights several critical security practices:

Input Validation and Whitelisting:

 Python example: Whitelist-based sort validation
ALLOWED_SORT_FIELDS = ['created_at', 'updated_at', 'title', 'severity']
ALLOWED_SORT_ORDERS = ['asc', 'desc']

def validate_sort_input(sort_query):
if not isinstance(sort_query, list):
raise ValueError("Sort must be a list")

for sort_item in sort_query:
if not isinstance(sort_item, dict):
raise ValueError("Each sort item must be an object")

for field, order in sort_item.items():
if field not in ALLOWED_SORT_FIELDS:
raise ValueError(f"Field '{field}' not allowed for sorting")
if order not in ALLOWED_SORT_ORDERS:
raise ValueError(f"Order '{order}' not allowed")

return True

Disable Dynamic Scripting in Production:

 elasticsearch.yml - Production security configuration
script.allowed_types: none  Disable all scripting
 OR, if scripts are absolutely necessary:
script.allowed_types: inline
script.allowed_contexts: score, update  Restrict to specific contexts

Use Parameterized Queries:

// Safe approach: Use params instead of embedding values in script source
{
"sort": {
"_script": {
"type": "number",
"script": {
"lang": "painless",
"source": "doc['field'].value  params.factor",
"params": {
"factor": 1.1
}
}
}
}
}

Implement API Gateway Filtering:

 GraphQL resolver with input sanitization
def resolve_search(self, info, sort_query=None):
if sort_query:
 Reject any sort_query containing script-like patterns
if any(pattern in str(sort_query).lower() for pattern in ['_script', 'painless', 'script']):
raise GraphQLError("Invalid sort parameter")

Validate against whitelist
validate_sort_input(sort_query)

Build Elasticsearch query safely
return perform_search(sort_query=sort_query)

Enable Elasticsearch Security Features:

Elasticsearch provides built-in security mechanisms that should be leveraged:

  • Sandbox Environment: Painless operates in a controlled sandbox with fine-grained allowlists
  • Seccomp (Linux) / Seatbelt (macOS) / ActiveProcessLimit (Windows): Prevents Elasticsearch from forking or running other processes
  • Script Type Restrictions: Set `script.allowed_types` to restrict inline vs. stored scripts
  • Context Restrictions: Use `script.allowed_contexts` to limit where scripts can run
  1. Lessons for Bug Hunters: Thinking Beyond the Obvious

This vulnerability underscores a crucial mindset for security researchers: don’t stop at “does the feature work?”—ask “what happens if my input reaches an internal engine?”

The attack chain demonstrates that:

  • GraphQL is just the entry point; the real danger lies in what happens downstream
  • Search engines like Elasticsearch, Solr, and Redis often have powerful scripting capabilities
  • AI models and internal APIs can be equally vulnerable to injection
  • A single, seemingly innocuous parameter can become a critical vulnerability

Bug Hunter’s Checklist for GraphQL Endpoints:

  1. Identify all parameters that accept user input, especially those passed to search/sort functions
  2. Test for injection by attempting to break out of expected data structures
  3. Explore scripting capabilities of backend systems (Elasticsearch Painless, MongoDB $where, etc.)
  4. Monitor response variations—changes in sort order or data presentation often indicate successful injection
  5. Document the full attack chain from input to execution to demonstrate impact

What Undercode Say

  • Trust Nothing, Validate Everything: The `sort_query` parameter was trusted implicitly because it “just” handled sorting. In modern API architectures, user input travels through multiple layers—GraphQL resolvers, database queries, search engines—and each layer is a potential injection point.
  • Scripting Engines Are Attack Surfaces: Elasticsearch’s Painless scripting language is designed for flexibility, but that same flexibility becomes a liability when user input reaches it. Always treat scripting engines as untrusted execution environments.
  • Defense in Depth Is Non-1egotiable: HackerOne’s fix—removing arbitrary JSON support and implementing schema validation—is the right approach. But the real lesson is that input validation should happen at every layer, not just at the API gateway.
  • Bug Bounty Economics: A $7,400 payout for what started as “just a sort parameter” demonstrates the value of creative, out-of-the-box thinking in security research. The most critical vulnerabilities often hide in the most unexpected places.
  • GraphQL Security Is Still Evolving: As GraphQL adoption grows, so does the attack surface. Organizations must treat GraphQL endpoints with the same rigor as traditional APIs, if not more, given their flexibility and introspection capabilities.

Prediction

-1 The HackerOne incident will not be an isolated case. As more organizations adopt GraphQL and integrate it with powerful search engines like Elasticsearch, similar vulnerabilities will emerge across the industry. The attack surface is simply too large and the integration points too numerous.

-1 Expect to see a wave of GraphQL + Elasticsearch injection disclosures in the coming months, as security researchers systematically probe sort parameters, aggregations, and other query-building features that traditionally accepted user input.

+1 On the positive side, this disclosure will accelerate the development of GraphQL security tooling and best practices. We’ll likely see new static analysis tools that can detect unsafe parameter passing from GraphQL resolvers to backend systems.

-1 However, the complexity of modern API stacks means that many organizations will struggle to implement proper input validation across all layers. The “API security debt” will continue to grow, particularly in organizations with rapidly evolving microservices architectures.

+1 The bug bounty model proved its value here—a critical vulnerability was identified, reported, and fixed before it could be exploited maliciously. This success story will encourage more organizations to invest in bug bounty programs and more researchers to explore complex, multi-layer attack vectors.

▶️ Related Video (78% Match):

🎯Let’s Practice For Free:

🎓 Live Courses & Certifications:

Join Undercode Academy for Verified Certifications

🚀 Request a Custom Project:

Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

IT/Security Reporter URL:

Reported By: Cybersecurity Bugbounty – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow UndercodeTesting & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin | 🦋BlueSky