Executive Summary
A critical SQL injection vulnerability (CVE-2026-41478, GHSA-jp74-mfrx-3qvh) was publicly disclosed on April 24, 2026, affecting all versions of Saltcorn — a popular open-source, no-code database application builder — prior to 1.4.6, 1.5.6, and 1.6.0-beta.5. Any authenticated user with as little as read access to a single table can exploit unsanitized parameters in the mobile-sync API routes to exfiltrate the full database, including admin password hashes and configuration secrets, or destroy application data entirely. Organizations running self-hosted Saltcorn instances should treat this as a P0 incident and patch immediately.
1. What Is This Vulnerability?
Saltcorn's mobile synchronization feature exposes two API routes that enable client devices to pull and push data changes:
POST /sync/load_changesPOST /sync/deletes
Both endpoints accept a JSON body containing a maxLoadedId parameter, which is used to filter records by row ID. The flaw is that Saltcorn does not enforce type validation or sanitize this parameter before interpolating it directly into a raw SQL query. An attacker can supply a crafted string value in place of an integer, and the database engine will execute the injected SQL as part of the query.
Because Saltcorn supports multiple backends (PostgreSQL, SQLite), the attack surface applies broadly regardless of which database the instance is using. With PostgreSQL, COPY TO, pg_read_file(), and stacked queries give an attacker powerful primitives for data extraction and system interaction.
Attack Vector
The attack requires only a valid authenticated session — no administrator rights, no special role. Any user who has been granted read access to at least one table (which is the default minimum permission level for most Saltcorn deployments) can craft a POST request to /sync/load_changes with a payload like:
{
"table": "users",
"maxLoadedId": "0 UNION SELECT email, password, 3, 4 FROM users--"
}
From this foothold, attackers can escalate to full Data Manipulation Language (DML) and Data Definition Language (DDL) execution — inserting, modifying, or deleting records, or dropping tables entirely.
Real-World Impact
As of publication, there are no confirmed public reports of active exploitation in the wild. However, given the extremely low attack complexity (CVSS AC:L, PR:L) and the fact that Saltcorn is commonly deployed by organizations that may not closely monitor low-privilege internal users, the risk of exploitation is high. The vulnerability was assigned a CVSS v3.1 score of 9.9 (Critical), with full impact scores on Confidentiality, Integrity, and Availability, plus a Changed Scope — meaning the impact can extend beyond the vulnerable component itself.
2. Who Is Affected?
All Saltcorn deployments running the following versions are vulnerable:
- All Saltcorn releases before 1.4.6 (stable branch)
- All Saltcorn releases before 1.5.6 (LTS branch)
- All Saltcorn releases before 1.6.0-beta.5 (beta branch)
Any deployment that exposes Saltcorn's mobile-sync API surface to authenticated users — including internal corporate tools, SaaS platforms built on Saltcorn, and citizen-developer environments — is at risk. Self-hosted instances with public-facing or intranet APIs are the primary attack surface.
You are affected if:
- You run a self-hosted Saltcorn instance on any of the above versions
- The
/sync/API routes are reachable by any authenticated user - You have not already blocked
/sync/*POST traffic at the network edge
You are likely safe if:
- You have already upgraded to Saltcorn 1.4.6, 1.5.6, or 1.6.0-beta.5
- You are using Saltcorn's fully managed cloud offering (patched by the vendor)
- You have a WAF rule or reverse-proxy configuration blocking all POST requests to
/sync/
3. How to Detect It (Testing)
Manual Testing Steps
-
Identify your Saltcorn version. Log in to the Saltcorn admin panel and navigate to Settings → About, or run
npm list @saltcorn/serverin your deployment directory. If the version predates 1.4.6 / 1.5.6 / 1.6.0-beta.5, you are vulnerable. -
Check for exposed sync routes. From any network location that your users can access, send:
curl -s -o /dev/null -w "%{http_code}" \ -X POST https://<your-saltcorn-host>/sync/load_changes \ -H "Cookie: <authenticated-session-cookie>" \ -H "Content-Type: application/json" \ -d '{"table":"<any-table-name>","maxLoadedId":1}'A
200response confirms the route is reachable and active. -
Test for SQL injection. Send a payload with a deliberately malformed
maxLoadedIdvalue (e.g., a string with a single quote):curl -s -X POST https://<your-saltcorn-host>/sync/load_changes \ -H "Cookie: <authenticated-session-cookie>" \ -H "Content-Type: application/json" \ -d '{"table":"<any-table-name>","maxLoadedId":"1'"'"'"}'A
500server error containing a database error message (e.g.,syntax error at or near "'") strongly indicates the parameter is being passed unsanitized to the database engine. -
Confirm column count via UNION probe. Iterate
ORDER BYclause integers (1, 2, 3…) until you get an error, confirming the number of columns returned by the base query — a prerequisite for a functional UNION-based injection.
Automated Scanning
Tool: SQLMap
sqlmap -u "https://<your-saltcorn-host>/sync/load_changes" \
--method POST \
--data '{"table":"<table>","maxLoadedId":"*"}' \
--headers "Content-Type: application/json\nCookie: <session-cookie>" \
--level 3 --risk 2 \
--dbms=PostgreSQL \
--batch
Expected output on a vulnerable host: SQLMap will identify the injection point in maxLoadedId, enumerate the database backend, and begin table/column extraction.
Tool: Burp Suite Pro
Use the Intruder or Active Scan module against the /sync/load_changes endpoint. Set maxLoadedId as the injection point using § markers in Repeater, and run the SQL Injection payload set from Burp's built-in library.
Tool: OWASP ZAP
Enable the Active Scan with "SQL Injection" rules enabled, point ZAP at your Saltcorn base URL with an authenticated session, and include /sync/ in the scope. ZAP will crawl and fuzz the endpoint automatically.
Code Review Checklist
If you have access to source code for a forked or customized Saltcorn deployment, audit the following:
- Search for
maxLoadedIdusage in/packages/saltcorn-data/and/packages/server/routes/— verify it is cast to integer before use - Confirm that sync route handlers use parameterized queries (
$1,?placeholders) rather than string interpolation - Check that all route-level middleware enforces authentication and table-level access control before reaching SQL logic
- Verify no raw
.query()calls exist in the sync handler that take user-controlled input as part of the query string
4. How to Fix It (Mitigation)
Step-by-Step Remediation
-
Identify your current Saltcorn version (see detection step above).
-
Upgrade to a patched release. Choose the branch appropriate for your deployment:
- Stable:
npm install -g @saltcorn/cli@1.4.6or update viasaltcorn upgrade - LTS: Upgrade to 1.5.6
- Beta testers: Upgrade to 1.6.0-beta.5
- Stable:
-
Restart the Saltcorn server process after upgrading to ensure the patched code is loaded.
-
Verify the upgrade. Re-run the version check and confirm the new version is reflected in the admin panel.
-
Review logs for prior exploitation. Search your database access logs and application logs for POST requests to
/sync/load_changesor/sync/deleteswith anomalousmaxLoadedIdvalues (strings, SQL keywords, long payloads) from any time window prior to your patch deployment. -
Rotate credentials if exploitation is suspected. If logs show suspicious activity, rotate the database credentials, admin account passwords, any API keys stored in Saltcorn configuration, and inform users of a potential data exposure.
Code Fix Example
The root cause is type coercion failure. The vulnerable pattern looks like:
// VULNERABLE — user input interpolated directly into query
const rows = await db.query(
`SELECT * FROM "${table._name}" WHERE id > ${req.body.maxLoadedId}`
);
The patched code enforces integer casting before use:
// SAFE — input is cast to integer; NaN/string values become 0
const maxLoadedId = parseInt(req.body.maxLoadedId, 10) || 0;
const rows = await db.query(
`SELECT * FROM "${table._name}" WHERE id > $1`,
[maxLoadedId]
);
This two-pronged fix (explicit parseInt + parameterized query) ensures that any non-integer input is neutralized before it reaches the SQL engine.
Configuration Hardening (Temporary Workaround)
If you cannot immediately patch, apply the following at the reverse-proxy layer to block the vulnerable routes from all clients until you can upgrade:
Nginx:
location ~* ^/sync/ {
limit_except GET {
deny all;
}
}
Or, to block all sync traffic until patched:
location ~* ^/sync/ {
return 403;
}
Apache:
<LocationMatch "^/sync/">
<LimitExcept GET>
Require all denied
</LimitExcept>
</LocationMatch>
AWS WAF / Cloudflare WAF: Create a managed rule or custom rule blocking POST/PUT/PATCH requests where the URI path starts with /sync/.
Note: Blocking the sync routes will disable the Saltcorn mobile app sync functionality. This is an acceptable temporary tradeoff until the patch can be applied.
5. How to Test the Fix (Validation)
Regression Test Scenarios
- Scenario A: Confirm that a valid, well-formed POST to
/sync/load_changeswith an integermaxLoadedIdreturns the expected records (mobile sync still works normally after patching). - Scenario B: Confirm that sending a string
maxLoadedIdvalue (e.g.,"abc") results in a safe fallback (e.g., returns an empty result or defaults to0) without triggering a database error. - Scenario C: Confirm that sending a SQL injection payload as
maxLoadedId(e.g.,"1 OR 1=1") does not return unexpected rows and does not produce a database syntax error in the response body.
Security Test Cases
Test Case 1: Verify SQL injection no longer executes
- Precondition: Saltcorn upgraded to 1.4.6 / 1.5.6 / 1.6.0-beta.5
- Steps:
- Authenticate as a low-privilege user.
- POST to
/sync/load_changeswith"maxLoadedId": "0 UNION SELECT password,2,3,4 FROM users--".
- Expected Result: Response is empty, returns only rows matching the base query criteria (none, since id > 0 UNION is now sanitized to id > 0), and no password hashes appear in the response body.
Test Case 2: Confirm type coercion
- Precondition: Saltcorn upgraded to patched version.
- Steps:
- Send
"maxLoadedId": "9999999; DROP TABLE users;--".
- Send
- Expected Result: The
userstable still exists, the response contains no error, and the query treats the input as the integer9999999.
Automated Tests
import requests
BASE_URL = "https://<your-saltcorn-host>"
SESSION_COOKIE = {"connect.sid": "<authenticated-session-cookie>"}
INJECTION_PAYLOADS = [
"0 UNION SELECT password,2,3,4 FROM users--",
"1; DROP TABLE users;--",
"1' OR '1'='1",
"1 OR 1=1",
]
def test_sqli_patched(table_name):
for payload in INJECTION_PAYLOADS:
resp = requests.post(
f"{BASE_URL}/sync/load_changes",
json={"table": table_name, "maxLoadedId": payload},
cookies=SESSION_COOKIE,
timeout=10,
)
assert resp.status_code != 500, f"Server error on payload: {payload}"
body = resp.text.lower()
assert "password" not in body, f"Possible data leak on payload: {payload}"
assert "syntax error" not in body, f"SQL error visible on payload: {payload}"
print(f"PASS: Payload safely handled — {payload[:40]}")
if __name__ == "__main__":
test_sqli_patched("<a-table-you-have-access-to>")
Run this test suite against your patched instance to confirm all injection vectors are blocked.
6. Prevention & Hardening
Best Practices
- Always use parameterized queries. Never interpolate user-supplied input directly into SQL strings. Use prepared statements or ORM abstractions that enforce query parameterization at the framework level.
- Apply strict input validation at the API boundary. Use JSON Schema or Joi-style validators on all incoming request bodies. For integer fields like
maxLoadedId, reject requests that supply non-integer values with a400 Bad Requestbefore the value reaches any data access layer. - Follow the principle of least privilege for database accounts. The Saltcorn application database user should have only the permissions it needs (SELECT/INSERT/UPDATE/DELETE on application tables) and never DDL rights (CREATE, DROP, ALTER). This limits the blast radius of any SQL injection that does get through.
- Subscribe to security advisories for all no-code and low-code platforms. Platforms like Saltcorn often receive fewer security eyes than mainstream frameworks. Monitor the Saltcorn GitHub Security Advisories and the GitHub Advisory Database for new disclosures.
- Include third-party platform dependencies in your vulnerability management program. No-code tools are still software. Treat them the same as any other application dependency — track versions, subscribe to changelogs, and define patching SLAs for critical/high severity findings (recommended: 24–72 hours for CVSS ≥ 9.0).
Monitoring & Detection
Set up alerting for the following log patterns to detect in-progress exploitation attempts against unpatched or future-vulnerable instances:
Application log patterns to alert on:
POST /sync/load_changes — body contains non-integer maxLoadedId
POST /sync/deletes — body contains SQL keywords (UNION, SELECT, DROP, INSERT)
HTTP 500 responses from /sync/* routes
Database-level detection (PostgreSQL):
Enable log_min_duration_statement = 0 and log_statement = 'all' in a test/staging environment to capture all queries. In production, use pg_stat_statements and alert on queries to the _sc_tables metadata tables or queries involving UNION SELECT originating from the application user.
Network-level detection:
Configure your WAF or IDS/IPS to flag POST requests to /sync/ containing URL-encoded SQL keywords (%27, %22, UNION, SELECT, DROP) in the body. Tools like ModSecurity with the OWASP Core Rule Set (CRS) will catch many common SQL injection patterns out of the box.
References
- CVE Entry: CVE-2026-41478 — CIRCL Vulnerability Lookup
- GitHub Security Advisory: GHSA-jp74-mfrx-3qvh — Authenticated SQL Injection in Saltcorn Mobile Sync Endpoints
- Technical Writeup: Saltcorn Mobile-Sync SQL Injection (CVE-2026-41478) — TheHackerWire
- Threat Intelligence: CVE-2026-41478 — THREATINT
- CVE Report: GHSA-jp74-mfrx-3qvh — CVEReports
- Patch — Saltcorn 1.4.6: Saltcorn Releases — GitHub
- OWASP SQL Injection Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html