Vulnerability Analysis

CVE-2026-41478: Saltcorn Mobile-Sync SQL Injection — Full Database Takeover via Low-Privilege Credentials

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_changes
  • POST /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

  1. Identify your Saltcorn version. Log in to the Saltcorn admin panel and navigate to Settings → About, or run npm list @saltcorn/server in your deployment directory. If the version predates 1.4.6 / 1.5.6 / 1.6.0-beta.5, you are vulnerable.

  2. 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 200 response confirms the route is reachable and active.

  3. Test for SQL injection. Send a payload with a deliberately malformed maxLoadedId value (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 500 server 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.

  4. Confirm column count via UNION probe. Iterate ORDER BY clause 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 maxLoadedId usage 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

  1. Identify your current Saltcorn version (see detection step above).

  2. Upgrade to a patched release. Choose the branch appropriate for your deployment:

    • Stable: npm install -g @saltcorn/cli@1.4.6 or update via saltcorn upgrade
    • LTS: Upgrade to 1.5.6
    • Beta testers: Upgrade to 1.6.0-beta.5
  3. Restart the Saltcorn server process after upgrading to ensure the patched code is loaded.

  4. Verify the upgrade. Re-run the version check and confirm the new version is reflected in the admin panel.

  5. Review logs for prior exploitation. Search your database access logs and application logs for POST requests to /sync/load_changes or /sync/deletes with anomalous maxLoadedId values (strings, SQL keywords, long payloads) from any time window prior to your patch deployment.

  6. 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_changes with an integer maxLoadedId returns the expected records (mobile sync still works normally after patching).
  • Scenario B: Confirm that sending a string maxLoadedId value (e.g., "abc") results in a safe fallback (e.g., returns an empty result or defaults to 0) 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:
    1. Authenticate as a low-privilege user.
    2. POST to /sync/load_changes with "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:
    1. Send "maxLoadedId": "9999999; DROP TABLE users;--".
  • Expected Result: The users table still exists, the response contains no error, and the query treats the input as the integer 9999999.

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 a 400 Bad Request before 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

Latest from the blog

See all →