{"id":"CVE-2026-63224","aliases":["GHSA-cgfv-jrfp-2r7v","CVE-2026-62238"],"title":"OpenRemote has authenticated SQL injection in crosstab datapoint exports","titleSource":"GitHub_M","descriptionSource":"OSV","declaredSeverity":"HIGH","description":"## Summary\n\nThe datapoint export API builds a PostgreSQL crosstab export query by concatenating asset display names into raw SQL. An authenticated user who can create or rename an asset and then request a crosstab datapoint export can inject SQL through the asset name. The injected query output is streamed back to the caller inside the normal ZIP/CSV export response.\n\nThis creates a practical database exfiltration primitive through the application API. In a multi-tenant deployment, this can expose data outside the attacker's tenant if the application database role can read shared manager tables.\n\n## Affected Component\n\n- Datapoint export endpoint for asset datapoints.\n- Crosstab export formats, specifically CSV crosstab-style exports.\n- Query builder path that constructs a `COPY (SELECT ... FROM crosstab(...)) TO STDOUT` statement.\n\n## Security Impact\n\nImpact is high. A remote authenticated attacker with asset read/write capabilities can:\n\n- Store SQL syntax inside an asset name.\n- Trigger the crosstab export path for that asset.\n- Cause the backend to execute attacker-influenced SQL through the PostgreSQL connection used by the manager service.\n- Receive injected `SELECT` results in the exported CSV contained in the ZIP response.\n\nThe demonstrated impact is database data exfiltration. The proof of concept safely retrieved database execution context and an aggregate table count. A real attacker could adapt the injected `SELECT` to read other database tables accessible to the application database role.\n\nThis is especially sensitive in multi-tenant deployments because application tables commonly contain data for multiple realms/tenants in the same database.\n\n## Attack Preconditions\n\nThe attacker needs:\n\n- A valid authenticated session.\n- Permission to create or rename at least one asset.\n- Permission to read/export datapoints for at least one attribute on that asset.\n- Access to a crosstab datapoint export format.\n\nNo direct database access is required. No server filesystem access is required. No token forgery is required.\n\n## Technical Details\n\nThe export implementation derives a crosstab header from the asset name and attribute name. It then embeds that header into two SQL contexts:\n\n1. A PostgreSQL double-quoted column identifier:\n\n```sql\n\"<asset name> : <attribute name>\" text\n```\n\n2. A category query passed to `crosstab(...)`, wrapped in a fixed dollar-quoted delimiter:\n\n```sql\n$cat$ SELECT header FROM (VALUES ('<asset name> : <attribute name>')) AS t(header) $cat$\n```\n\nThe current escaping is incomplete:\n\n- Single quotes are escaped in one string-literal context.\n- Double quotes in asset names are not escaped before being placed inside quoted identifiers.\n- The fixed dollar-quote delimiter is not protected against an asset name containing the delimiter token.\n\nAs a result, an attacker-controlled asset name can break out of the intended SQL grammar boundary and append SQL to the generated `COPY ... TO STDOUT` query. Because the backend streams `COPY` output into the export response, injected query rows are returned to the attacker as CSV.\n\n## Example Exploit Flow\n\n1. Authenticate normally.\n2. Create or rename an asset using a name containing SQL metacharacters that closes the crosstab column definition.\n3. Ensure the asset has an exportable datapoint attribute.\n4. Write at least one datapoint value for that attribute, if necessary.\n5. Request a CSV crosstab datapoint export for the crafted asset attribute.\n6. Inspect the returned ZIP/CSV. The CSV contains both normal datapoint rows and rows produced by the injected SQL.\n\nA safe proof query demonstrated exfiltration of:\n\n- `current_user`\n- `current_database()`\n- `count(*)` from an application table\n\nThe returned CSV contained a row equivalent to:\n\n```text\n<timestamp>,<database_user>:<database_name>:<table_count>\n```\n\n## Root Cause\n\nThe root cause is manual SQL string construction using user-controlled display data as SQL syntax.\n\nThe asset name is treated as presentation data in the application model, but later reused as part of executable SQL:\n\n- As an SQL identifier in the crosstab output column list.\n- As a value inside a category query string passed to PostgreSQL.\n\nThese contexts require different escaping rules. Applying partial string escaping is error-prone and currently misses exploitable grammar boundaries.\n\n## Recommended Fix\n\nAvoid embedding user-controlled asset names directly into executable SQL.\n\nRecommended options:\n\n1. Do not use asset names as SQL identifiers.\n   - Generate deterministic internal column aliases such as `c1`, `c2`, `c3`.\n   - Keep the user-facing asset/attribute labels outside SQL and apply them only when serializing CSV headers.\n\n2. If dynamic identifiers are unavoidable, quote them using a database-aware identifier quoting function.\n   - For PostgreSQL identifiers, double embedded `\"` characters.\n   - Do not perform ad hoc quoting with string concatenation.\n\n3. Avoid fixed dollar-quote delimiters around attacker-influenced content.\n   - Use prepared statements or server-side functions where possible.\n   - If textual SQL must be generated, choose a delimiter that cannot appear in user input or escape/validate before use.\n\n4. Add a strict validation boundary for display names if the product can tolerate it.\n   - This should be defense-in-depth, not the only fix.\n   - Reject control characters and SQL-significant delimiter sequences in asset names if they are not required.\n\n5. Add regression tests for:\n   - Asset names containing `\"`.\n   - Asset names containing the fixed dollar-quote delimiter.\n   - Asset names containing newline/comment syntax.\n   - Crosstab exports with multiple assets and attributes.\n   - Confirmation that returned CSV never contains injected query output.\n\n## Suggested Safe Design\n\nBuild the crosstab with internal, non-user-controlled category keys and column names. For example:\n\n- Use asset IDs and attribute names only as parameterized data for filtering.\n- Generate internal column identifiers such as `col_0`, `col_1`.\n- Maintain a separate mapping from `col_0` to the display label.\n- Replace the CSV header row after query execution using application-side serialization, not SQL identifiers derived from user input.\n\nThis removes asset display names from SQL syntax entirely.\n\n## Severity\n\nSuggested severity: High\n\nRationale:\n\n- Network reachable through the authenticated API.\n- Low attack complexity after authentication.\n- Requires only ordinary asset read/write/export capabilities.\n- Demonstrated SQL injection result exfiltration through a normal application response.\n- High confidentiality impact due to possible cross-tenant database reads.\n- Integrity and availability impact were not required for the demonstrated exploit and should be assessed separately based on the database role's privileges.","severity":"high","score":7.2,"published":"2026-07-06T21:51:32.000Z","modified":"2026-10-31T10:55:53.653Z","withdrawn":false,"knownExploited":false,"affected":[{"ecosystem":"Maven","name":"io.openremote:openremote-manager","sources":[{"name":"OSV · GHSA-cgfv-jrfp-2r7v","url":"https://osv.dev/vulnerability/GHSA-cgfv-jrfp-2r7v"}],"ranges":[{"events":[{"introduced":"0"},{"fixed":"1.26.0"}],"type":"ECOSYSTEM"}],"versions":["1.10.0","1.11.0","1.11.1","1.11.2","1.11.3","1.12.0","1.12.1","1.12.3","1.13.0","1.13.1","1.14.0","1.15.0","1.15.1","1.15.2","1.16.1","1.17.0","1.17.1","1.17.2","1.17.3","1.18.0","1.19.0","1.2.0","1.2.1","1.2.2","1.2.3","1.2.4","1.20.0","1.20.1","1.20.2","1.21.0","1.22.0","1.22.1","1.23.0","1.23.1","1.24.0","1.24.1","1.24.2","1.25.0","1.3.0","1.3.1","1.3.2","1.3.3","1.3.4","1.3.5","1.4.0","1.5.0","1.6.0","1.6.2","1.6.3","1.6.4","1.6.5","1.7.0","1.8.0","1.8.1","1.9.0"],"fixed":["1.26.0"]}],"sourceAffected":[],"references":["https://github.com/openremote/core/security/advisories/GHSA-cgfv-jrfp-2r7v","https://github.com/openremote/core/commit/02ac83074b81617add814b2a72d459abdf374147","https://github.com/openremote/core/releases/tag/1.26.0","https://github.com/openremote/openremote/security/advisories/GHSA-cgfv-jrfp-2r7v","https://github.com/openremote/openremote/commit/02ac83074b81617add814b2a72d459abdf374147","https://github.com/openremote/openremote"],"sources":[{"name":"CVE","id":"CVE-2026-63224","modified":"2026-10-30T22:05:43.581Z","url":"https://www.cve.org/CVERecord?id=CVE-2026-63224"},{"name":"OSV","id":"GHSA-cgfv-jrfp-2r7v","modified":"2026-10-31T10:55:53.653622012Z","url":"https://osv.dev/vulnerability/GHSA-cgfv-jrfp-2r7v"}],"assessments":[{"source":"GitHub_M","vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:L/SC:N/SI:N/SA:N","score":7.2},{"source":"OSV","vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:L/SC:N/SI:N/SA:N","score":7.2}],"cve":{"records":[{"id":"CVE-2026-63224","source":"GitHub_M","state":"PUBLISHED","reasons":[],"replacements":[],"disputed":false,"unsupportedWhenAssigned":false,"reserved":"2026-07-15T22:19:06.907Z","published":"2026-10-30T22:05:43.581Z","modified":"2026-10-30T22:05:43.581Z"}],"publishers":[{"name":"GitHub_M","role":"CNA","organizationId":"a0819718-46f1-4df5-94e2-005712e83aaa","modified":"2026-10-30T22:05:43.581Z"}],"references":[{"url":"https://github.com/openremote/core/security/advisories/GHSA-cgfv-jrfp-2r7v","tags":["x_refsource_CONFIRM"],"source":"GitHub_M"},{"url":"https://github.com/openremote/core/commit/02ac83074b81617add814b2a72d459abdf374147","tags":["x_refsource_MISC"],"source":"GitHub_M"},{"url":"https://github.com/openremote/core/releases/tag/1.26.0","tags":["x_refsource_MISC"],"source":"GitHub_M"}],"weaknesses":[{"id":"CWE-89","description":"CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')","source":"GitHub_M"}],"applicability":[],"prioritization":[]},"products":[{"vendor":"openremote","name":"openremote","platforms":[],"versions":[{"version":"< 1.26.0","status":"affected"}],"cpes":[],"source":"GitHub_M","url":"https://www.cve.org/CVERecord?id=CVE-2026-63224"}],"remediation":[],"observedAt":"2026-10-31T12:49:30.857Z","kev":{"status":"not-listed","checkedAt":"2026-10-31T14:10:32.801Z"},"coverage":[{"source":"NVD","status":"available","checkedAt":"2026-10-30T23:20:34.441Z","attemptedAt":"2026-10-31T12:50:36.565Z"},{"source":"GHSA","status":"available","checkedAt":"2026-10-30T23:20:34.358Z","attemptedAt":"2026-10-31T12:50:40.043Z"},{"source":"EUVD","status":"available","checkedAt":"2026-10-30T23:20:35.181Z","attemptedAt":"2026-10-31T12:50:38.410Z"},{"source":"EPSS","status":"available","checkedAt":"2026-10-30T23:20:34.253Z","attemptedAt":"2026-10-31T12:50:36.780Z"}],"evidence":[{"source":"NVD","id":"CVE-2026-63224","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-63224","modified":"2026-10-30T22:16:58.987","score":7.2,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","checkedAt":"2026-10-30T23:20:34.441Z"},{"source":"NVD","id":"CVE-2026-62238","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-62238","modified":"2026-10-24T13:10:00.147","score":7.2,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","checkedAt":"2026-10-31T12:50:36.565Z"},{"source":"NVD","id":"CVE-2026-62238","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-62238","modified":"2026-10-24T13:10:00.147","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H","checkedAt":"2026-10-31T12:50:36.565Z"},{"source":"FIRST EPSS","id":"CVE-2026-62238","url":"https://www.first.org/epss/","probability":0.00498,"percentile":0.4266,"date":"2026-10-30","checkedAt":"2026-10-31T12:50:36.780Z"},{"source":"ENISA EUVD","id":"EUVD-2026-45084","url":"https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-45084","modified":"Oct 8, 2026, 3:23:45 PM","score":7.2,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:L/SC:N/SI:N/SA:N","checkedAt":"2026-10-31T12:50:38.410Z"},{"source":"GitHub Advisory Database","id":"GHSA-rw7h-h988-gfff","url":"https://github.com/advisories/GHSA-rw7h-h988-gfff","modified":"2026-07-30T15:32:14Z","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H","checkedAt":"2026-10-31T12:50:40.043Z"}],"aiSummary":null}