GHSA-5MX2-W598-339M
Vulnerability from github – Published: 2026-02-18 22:40 – Updated: 2026-02-18 22:40Summary
A query injection vulnerability exists in the @langchain/langgraph-checkpoint-redis package's filter handling. The RedisSaver and ShallowRedisSaver classes construct RediSearch queries by directly interpolating user-provided filter keys and values without proper escaping. RediSearch has special syntax characters that can modify query behavior, and when user-controlled data contains these characters, the query logic can be manipulated to bypass intended access controls.
Attack surface
The core vulnerability was in the list() methods of both RedisSaver and ShallowRedisSaver: these methods failed to escape RediSearch special characters in filter keys and values when constructing queries. When unescaped data containing RediSearch syntax was used, the injected operators were interpreted by RediSearch rather than treated as literal search values.
This escaping bug enabled the following attack vector:
- Thread boundary escape via OR operator: RediSearch uses
|as an OR operator with specific precedence rules. A query likeA B | Cis interpreted as(A AND B) OR C. By injecting}) | (@thread_id:{*into a filter value, an attacker can append an OR clause that matches all threads, effectively bypassing the thread isolation constraint.
The injected query (@thread_id:{legitimate-thread}) (@source:{x}) | (@thread_id:{*}) matches:
- Documents with
thread_id:legitimate-thread AND source:x, OR - Documents with ANY
thread_id
The second clause matches all threads, bypassing thread isolation entirely.
Who is affected?
Applications are vulnerable if they:
- Pass user-controlled input to filter parameters — When using
getStateHistory()orcheckpointer.list()with filter values derived from user input, HTTP parameters, or other untrusted sources. - Use Redis checkpointing in multi-tenant applications — Applications that rely on thread isolation to separate data between users or tenants are at risk of cross-tenant data access.
The most common attack vector is through API endpoints that expose filtering capabilities to end users, allowing them to search or filter their conversation history.
Impact
Attackers who control filter input can bypass thread isolation by injecting RediSearch OR operators to construct queries that match all threads regardless of the intended thread constraint. This enables access to checkpoint data from threads the attacker is not authorized to view.
Key severity factors:
- Enables complete bypass of thread-based access controls
- Sensitive conversation data from other users may be exposed
- Affects multi-tenant applications relying on thread isolation for data separation
- Requires only control over filter input values (common in user-facing APIs)
Exploit example
import { RedisSaver } from "@langchain/langgraph-checkpoint-redis";
const saver = new RedisSaver({ /* redis config */ });
// Normal usage - should only see thread "user-123-thread"
const legitHistory = saver.list({
configurable: { thread_id: "user-123-thread" }
}, {
filter: { source: "loop" }
});
// Attacker crafts malicious filter value
const attackerFilter = {
source: "x}) | (@thread_id:{*" // Injects OR clause matching ALL threads
};
// This produces a query like:
// (@thread_id:{user-123-thread}) (@source:{x}) | (@thread_id:{*})
// Due to precedence, this matches ALL threads!
const stolenHistory = saver.list({
configurable: { thread_id: "user-123-thread" }
}, {
filter: attackerFilter
});
// stolenHistory now contains checkpoints from ALL threads - DATA LEAKED!
Security hardening changes
The 1.0.2 patch introduces the following changes:
- Escape utility function: A new
escapeRediSearchTagValue()function properly escapes all RediSearch special characters (- . < > { } [ ] " ' : ; ! @ # $ % ^ & * ( ) + = ~ | \ ? /) by prefixing them with backslashes. - Filter key escaping: All filter keys are escaped before being used in query construction.
- Filter value escaping: All filter values are escaped before being interpolated into RediSearch tag queries.
Migration guide
No changes needed for most users
The fix is backward compatible. Existing code will work without modifications—filter values that previously worked will continue to work, with the added protection against injection:
import { RedisSaver } from "@langchain/langgraph-checkpoint-redis";
// Works exactly as before, now with injection protection
const history = saver.list(config, {
filter: { source: "loop" }
});
If you were relying on special characters
If your application intentionally used RediSearch syntax in filter values (unlikely but possible), be aware that these characters will now be escaped and treated as literals.
For applications with user-facing filters
No code changes required, but this is a good time to review your API design:
// Before: Vulnerable to injection
app.get("/history", async (req, res) => {
const history = await saver.list(config, {
filter: req.query.filter // User-controlled - was vulnerable
});
});
// After: Now safe, but consider validating allowed filter keys
app.get("/history", async (req, res) => {
const allowedKeys = ["source", "step"];
const sanitizedFilter = Object.fromEntries(
Object.entries(req.query.filter || {})
.filter(([key]) => allowedKeys.includes(key))
);
const history = await saver.list(config, {
filter: sanitizedFilter
});
});
Recommendation: Even with the fix in place, consider validating that filter keys are from an allowed list as a defense-in-depth measure.
References
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@langchain/langgraph-checkpoint-redis"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.0.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-27022"
],
"database_specific": {
"cwe_ids": [
"CWE-74"
],
"github_reviewed": true,
"github_reviewed_at": "2026-02-18T22:40:09Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nA query injection vulnerability exists in the `@langchain/langgraph-checkpoint-redis` package\u0027s filter handling. The `RedisSaver` and `ShallowRedisSaver` classes construct RediSearch queries by directly interpolating user-provided filter keys and values without proper escaping. RediSearch has special syntax characters that can modify query behavior, and when user-controlled data contains these characters, the query logic can be manipulated to bypass intended access controls.\n\n## Attack surface\n\nThe core vulnerability was in the `list()` methods of both `RedisSaver` and `ShallowRedisSaver`: these methods failed to escape RediSearch special characters in filter keys and values when constructing queries. When unescaped data containing RediSearch syntax was used, the injected operators were interpreted by RediSearch rather than treated as literal search values.\n\nThis escaping bug enabled the following attack vector:\n\n- **Thread boundary escape via OR operator**: RediSearch uses `|` as an OR operator with specific precedence rules. A query like `A B | C` is interpreted as `(A AND B) OR C`. By injecting `}) | (@thread_id:{*` into a filter value, an attacker can append an OR clause that matches all threads, effectively bypassing the thread isolation constraint.\n\nThe injected query `(@thread_id:{legitimate-thread}) (@source:{x}) | (@thread_id:{*})` matches:\n\n- Documents with `thread_id:legitimate-thread AND source:x`, OR\n- Documents with ANY `thread_id`\n\nThe second clause matches all threads, bypassing thread isolation entirely.\n\n## Who is affected?\n\nApplications are vulnerable if they:\n\n- **Pass user-controlled input to filter parameters** \u2014 When using `getStateHistory()` or `checkpointer.list()` with filter values derived from user input, HTTP parameters, or other untrusted sources.\n- **Use Redis checkpointing in multi-tenant applications** \u2014 Applications that rely on thread isolation to separate data between users or tenants are at risk of cross-tenant data access.\n\nThe most common attack vector is through API endpoints that expose filtering capabilities to end users, allowing them to search or filter their conversation history.\n\n## Impact\n\nAttackers who control filter input can bypass thread isolation by injecting RediSearch OR operators to construct queries that match all threads regardless of the intended thread constraint. This enables access to checkpoint data from threads the attacker is not authorized to view.\n\nKey severity factors:\n\n- Enables complete bypass of thread-based access controls\n- Sensitive conversation data from other users may be exposed\n- Affects multi-tenant applications relying on thread isolation for data separation\n- Requires only control over filter input values (common in user-facing APIs)\n\n## Exploit example\n\n```typescript\nimport { RedisSaver } from \"@langchain/langgraph-checkpoint-redis\";\n\nconst saver = new RedisSaver({ /* redis config */ });\n\n// Normal usage - should only see thread \"user-123-thread\"\nconst legitHistory = saver.list({\n configurable: { thread_id: \"user-123-thread\" }\n}, {\n filter: { source: \"loop\" }\n});\n\n// Attacker crafts malicious filter value\nconst attackerFilter = {\n source: \"x}) | (@thread_id:{*\" // Injects OR clause matching ALL threads\n};\n\n// This produces a query like:\n// (@thread_id:{user-123-thread}) (@source:{x}) | (@thread_id:{*})\n// Due to precedence, this matches ALL threads!\n\nconst stolenHistory = saver.list({\n configurable: { thread_id: \"user-123-thread\" }\n}, {\n filter: attackerFilter\n});\n\n// stolenHistory now contains checkpoints from ALL threads - DATA LEAKED!\n```\n\n## Security hardening changes\n\nThe 1.0.2 patch introduces the following changes:\n\n- **Escape utility function**: A new `escapeRediSearchTagValue()` function properly escapes all RediSearch special characters (`- . \u003c \u003e { } [ ] \" \u0027 : ; ! @ # $ % ^ \u0026 * ( ) + = ~ | \\ ? /`) by prefixing them with backslashes.\n- **Filter key escaping**: All filter keys are escaped before being used in query construction.\n- **Filter value escaping**: All filter values are escaped before being interpolated into RediSearch tag queries.\n\n## Migration guide\n\n### No changes needed for most users\n\nThe fix is backward compatible. Existing code will work without modifications\u2014filter values that previously worked will continue to work, with the added protection against injection:\n\n```typescript\nimport { RedisSaver } from \"@langchain/langgraph-checkpoint-redis\";\n\n// Works exactly as before, now with injection protection\nconst history = saver.list(config, {\n filter: { source: \"loop\" }\n});\n```\n\n### If you were relying on special characters\n\nIf your application intentionally used RediSearch syntax in filter values (unlikely but possible), be aware that these characters will now be escaped and treated as literals.\n\n### For applications with user-facing filters\n\nNo code changes required, but this is a good time to review your API design:\n\n```typescript\n// Before: Vulnerable to injection\napp.get(\"/history\", async (req, res) =\u003e {\n const history = await saver.list(config, {\n filter: req.query.filter // User-controlled - was vulnerable\n });\n});\n\n// After: Now safe, but consider validating allowed filter keys\napp.get(\"/history\", async (req, res) =\u003e {\n const allowedKeys = [\"source\", \"step\"];\n const sanitizedFilter = Object.fromEntries(\n Object.entries(req.query.filter || {})\n .filter(([key]) =\u003e allowedKeys.includes(key))\n );\n const history = await saver.list(config, {\n filter: sanitizedFilter\n });\n});\n```\n\n\u003e **Recommendation**: Even with the fix in place, consider validating that filter keys are from an allowed list as a defense-in-depth measure.\n\n## References\n\n- [RediSearch Query Syntax](https://redis.io/docs/interact/search-and-query/query/)\n- [LangGraph Checkpoint Documentation](https://langchain-ai.github.io/langgraphjs/)",
"id": "GHSA-5mx2-w598-339m",
"modified": "2026-02-18T22:40:09Z",
"published": "2026-02-18T22:40:09Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/langchain-ai/langgraphjs/security/advisories/GHSA-5mx2-w598-339m"
},
{
"type": "WEB",
"url": "https://github.com/langchain-ai/langgraphjs/pull/1943"
},
{
"type": "WEB",
"url": "https://github.com/langchain-ai/langgraphjs/commit/814c76dc3938d0f6f7e17ca3bc11d6a12270b2a1"
},
{
"type": "PACKAGE",
"url": "https://github.com/langchain-ai/langgraphjs"
},
{
"type": "WEB",
"url": "https://github.com/langchain-ai/langgraphjs/releases/tag/@langchain/langgraph-checkpoint-redis@1.0.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "RediSearch Query Injection in @langchain/langgraph-checkpoint-redis"
}
Sightings
| Author | Source | Type | Date |
|---|
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.