Arches vulnerable to SQL Injection
Description
Arches is a web platform for creating, managing, & visualizing geospatial data. Versions prior to 6.1.2, 6.2.1, and 7.1.2 are vulnerable to SQL Injection. With a carefully crafted web request, it's possible to execute certain unwanted sql statements against the database. This issue is fixed in version 7.12, 6.2.1, and 6.1.2. Users are recommended to upgrade as soon as possible. There are no workarounds.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Arches before 6.1.2, 6.2.1, and 7.1.2 contains blind SQL injection flaws in paged_dropdown and get_child_edges, allowing attackers to extract database contents via crafted requests.
Vulnerability
Overview
CVE-2022-41892 is a blind SQL injection vulnerability affecting the Arches web platform for geospatial data management, prior to versions 6.1.2, 6.2.1, and 7.1.2 [1][2]. The root cause is unsafe string interpolation in the construction of SQL queries, particularly in the paged_dropdown endpoint and the get_child_edges function [1][2]. The developer fix replaced direct Python string formatting with parameterized queries using psycopg2's AsIs and %(name)s placeholders, preventing injection [1].
Attack
Vector and Requirements
The vulnerability is exploitable remotely without authentication, as the endpoints are accessible via the web API [2]. An attacker can inject arbitrary SQL by crafting malicious input into parameters such as the query value in paged_dropdown or the relationtypes variable [1][2]. Because these are blind injections, the attacker typically uses time-based payloads (e.g., SLEEP functions) to infer boolean results based on response timing, allowing step-by-step data extraction [2].
Impact
Successful exploitation allows an attacker to execute arbitrary SQL statements against the underlying PostgreSQL database [2]. This can lead to unauthorized reading of sensitive data (including user credentials and protected geospatial records), potential data modification, and possible database compromise, as the database connection runs with the application's privileges [2][3].
Mitigation
Status
The vulnerability was fixed in the stable/7.1.1 branch via commit 7ed53e23a616edf3301d95814d9d64de5e3072a9, and the official fix is included in versions 7.1.2, 6.2.1, and 6.1.2 [1][3]. Users are strongly advised to upgrade immediately; no workarounds exist [3]. The issue was responsibly disclosed by GitHub Security Lab (GHSL-2022-070) in September 2022, and a coordinated fix was published in November 2022 [2].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
archesPyPI | < 6.1.2 | 6.1.2 |
archesPyPI | >= 6.2.0, < 6.2.1 | 6.2.1 |
archesPyPI | >= 7.0.0, < 7.2.0 | 7.2.0 |
Affected products
3<7.1.2, <6.2.1, <6.1.2+ 1 more
- (no CPE)range: <7.1.2, <6.2.1, <6.1.2
- (no CPE)range: <= 6.1.2
Patches
17ed53e23a616fixes #9019, #9021, #9026 in stable/7.1.1
2 files changed · +126 −118
arches/app/models/concept.py+108 −99 modified@@ -32,6 +32,8 @@ from django.utils.translation import ugettext as _ from django.utils.translation import get_language from django.db import IntegrityError +from psycopg2.extensions import AsIs + import logging @@ -505,13 +507,12 @@ def get_child_edges( except: return [] - languageid = get_language() if languageid is None else languageid + # this interpolation is safe because `relationtypes` is hardcoded in all calls, and not accessible via the API relationtypes = " or ".join(["r.relationtype = '%s'" % (relationtype) for relationtype in relationtypes]) - depth_limit = "and depth < %s" % depth_limit if depth_limit else "" - child_valuetypes = ("','").join( - child_valuetypes if child_valuetypes else models.DValueType.objects.filter(category="label").values_list("valuetype", flat=True) - ) - limit_clause = " limit %s offset %s" % (limit, offset) if offset is not None else "" + offset_clause = " limit %(limit)s offset %(offset)s" if offset else "" + depth_clause = " and depth < %(depth_limit)s" if depth_limit else "" + + cursor = connection.cursor() if order_hierarchically: sql = """ @@ -525,9 +526,9 @@ def get_child_edges( WHERE conceptid=r.conceptidto AND valuetype in ('prefLabel') ORDER BY ( - CASE WHEN languageid = '{languageid}' THEN 10 - WHEN languageid like '{short_languageid}%' THEN 5 - WHEN languageid like '{default_languageid}%' THEN 2 + CASE WHEN languageid = %(languageid)s THEN 10 + WHEN languageid like %(short_languageid)s THEN 5 + WHEN languageid like %(default_languageid)s THEN 2 ELSE 0 END ) desc limit 1 @@ -547,8 +548,8 @@ def get_child_edges( limit 1 ) as collector FROM relations r - WHERE r.conceptidfrom = '{conceptid}' - and ({relationtypes}) + WHERE r.conceptidfrom = %(conceptid)s + and (%(relationtypes)s) ORDER BY sortorder, valuesto ) UNION @@ -559,9 +560,9 @@ def get_child_edges( WHERE conceptid=r.conceptidto AND valuetype in ('prefLabel') ORDER BY ( - CASE WHEN languageid = '{languageid}' THEN 10 - WHEN languageid like '{short_languageid}%' THEN 5 - WHEN languageid like '{default_languageid}%' THEN 2 + CASE WHEN languageid = %(languageid)s THEN 10 + WHEN languageid like %(short_languageid)s THEN 5 + WHEN languageid like %(default_languageid)s THEN 2 ELSE 0 END ) desc limit 1 @@ -582,7 +583,7 @@ def get_child_edges( ) as collector FROM relations r JOIN ordered_relationships b ON(b.conceptidto = r.conceptidfrom) - WHERE ({relationtypes}) + WHERE (%(relationtypes)s) ORDER BY sortorder, valuesto ) ), @@ -593,17 +594,17 @@ def get_child_edges( r.collector, 1 AS depth ---|NonRecursive Part FROM ordered_relationships r - WHERE r.conceptidfrom = '{conceptid}' - and ({relationtypes}) + WHERE r.conceptidfrom = %(conceptid)s + and (%(relationtypes)s) UNION SELECT r.conceptidfrom, r.conceptidto, row || '-' || to_char(row_number() OVER (), 'fm000000'), r.collector, depth+1 ---|RecursivePart FROM ordered_relationships r JOIN children b ON(b.conceptidto = r.conceptidfrom) - WHERE ({relationtypes}) - {depth_limit} + WHERE (%(relationtypes)s) + {depth_clause} ) {subquery} @@ -614,70 +615,73 @@ def get_child_edges( FROM ( SELECT * FROM values - WHERE conceptid={recursive_table}.conceptidto + WHERE conceptid=%(recursive_table)s.conceptidto AND valuetype in ('prefLabel') ORDER BY ( - CASE WHEN languageid = '{languageid}' THEN 10 - WHEN languageid like '{short_languageid}%' THEN 5 - WHEN languageid like '{default_languageid}%' THEN 2 + CASE WHEN languageid = %(languageid)s THEN 10 + WHEN languageid like %(short_languageid)s THEN 5 + WHEN languageid like %(default_languageid)s THEN 2 ELSE 0 END ) desc limit 1 ) d ) as valueto, depth, collector, count(*) OVER() AS full_count - FROM {recursive_table} order by row {limit_clause}; - + FROM %(recursive_table)s order by row {offset_clause}; """ - subquery = ( - """, results as ( - SELECT c.conceptidfrom, c.conceptidto, c.row, c.depth, c.collector - FROM children c - JOIN values ON(values.conceptid = c.conceptidto) - WHERE LOWER(values.value) like '%%%s%%' - AND values.valuetype in ('prefLabel') - UNION - SELECT c.conceptidfrom, c.conceptidto, c.row, c.depth, c.collector - FROM children c - JOIN results r on (r.conceptidfrom=c.conceptidto) - )""" - % query.lower() - if query is not None - else "" - ) + if query: + subquery = """ + , results as ( + SELECT c.conceptidfrom, c.conceptidto, c.row, c.depth, c.collector + FROM children c + JOIN values ON(values.conceptid = c.conceptidto) + WHERE LOWER(values.value) like %(query)s + AND values.valuetype in ('prefLabel') + UNION + SELECT c.conceptidfrom, c.conceptidto, c.row, c.depth, c.collector + FROM children c + JOIN results r on (r.conceptidfrom=c.conceptidto) + ) + """ + else: + subquery = "" + + sql = sql.format(subquery=subquery, offset_clause=offset_clause, depth_clause=depth_clause) recursive_table = "results" if query else "children" + languageid = get_language() if languageid is None else languageid - sql = sql.format( - conceptid=conceptid, - relationtypes=relationtypes, - child_valuetypes=child_valuetypes, - parent_valuetype=parent_valuetype, - depth_limit=depth_limit, - limit_clause=limit_clause, - subquery=subquery, - recursive_table=recursive_table, - languageid=languageid, - short_languageid=languageid.split("-")[0], - default_languageid=settings.LANGUAGE_CODE, + cursor.execute( + sql, + { + "conceptid": conceptid, + "relationtypes": AsIs(relationtypes), + "depth_limit": depth_limit, + "limit": limit, + "offset": offset, + "query": "%" + query.lower() + "%", + "recursive_table": AsIs(recursive_table), + "languageid": languageid, + "short_languageid": languageid.split("-")[0] + "%", + "default_languageid": settings.LANGUAGE_CODE + "%", + }, ) - else: sql = """ WITH RECURSIVE children AS ( SELECT r.conceptidfrom, r.conceptidto, r.relationtype, 1 AS depth FROM relations r - WHERE r.conceptidfrom = '{conceptid}' - AND ({relationtypes}) + WHERE r.conceptidfrom = %(conceptid)s + AND (%(relationtypes)s) UNION SELECT r.conceptidfrom, r.conceptidto, r.relationtype, depth+1 FROM relations r JOIN children c ON(c.conceptidto = r.conceptidfrom) - WHERE ({relationtypes}) - {depth_limit} + WHERE (%(relationtypes)s) + {depth_clause} ), results AS ( SELECT @@ -692,14 +696,15 @@ def get_child_edges( JOIN children c ON(c.conceptidto = valueto.conceptid) JOIN values valuefrom ON(c.conceptidfrom = valuefrom.conceptid) JOIN d_value_types dtypesfrom ON(dtypesfrom.valuetype = valuefrom.valuetype) - WHERE valueto.valuetype in ('{child_valuetypes}') - AND valuefrom.valuetype in ('{child_valuetypes}') + WHERE valueto.valuetype in (%(child_valuetypes)s) + AND valuefrom.valuetype in (%(child_valuetypes)s) ) - SELECT distinct {columns} - FROM results {limit_clause} - + SELECT distinct %(columns)s + FROM results {offset_clause} """ + sql = sql.format(offset_clause=offset_clause, depth_clause=depth_clause) + if not columns: columns = """ conceptidfrom::text, conceptidto::text, @@ -710,19 +715,24 @@ def get_child_edges( categoryfrom, categoryto """ - sql = sql.format( - conceptid=conceptid, - relationtypes=relationtypes, - child_valuetypes=child_valuetypes, - columns=columns, - depth_limit=depth_limit, - limit_clause=limit_clause, + cursor.execute( + sql, + { + "conceptid": conceptid, + "relationtypes": AsIs(relationtypes), + "child_valuetypes": ("','").join( + child_valuetypes + if child_valuetypes + else models.DValueType.objects.filter(category="label").values_list("valuetype", flat=True) + ), + "columns": AsIs(columns), + "depth_limit": depth_limit, + "limit": limit, + "offset": offset, + }, ) - cursor = connection.cursor() - cursor.execute(sql) - rows = cursor.fetchall() - return rows + return cursor.fetchall() def traverse(self, func, direction="down", scope=None, **kwargs): """ @@ -1176,30 +1186,31 @@ def get_e55_domain(self, conceptid): """ cursor = connection.cursor() - - sql = """ - WITH RECURSIVE children AS ( - SELECT d.conceptidfrom, d.conceptidto, c2.value, c2.valueid as valueid, c.value as valueto, c.valueid as valueidto, c.valuetype as vtype, 1 AS depth, array[d.conceptidto] AS conceptpath, array[c.valueid] AS idpath ---|NonRecursive Part - FROM relations d - JOIN values c ON(c.conceptid = d.conceptidto) - JOIN values c2 ON(c2.conceptid = d.conceptidfrom) - WHERE d.conceptidfrom = '{0}' - and c2.valuetype = 'prefLabel' - and c.valuetype in ('prefLabel', 'sortorder', 'collector') - and (d.relationtype = 'member' or d.relationtype = 'hasTopConcept') - UNION - SELECT d.conceptidfrom, d.conceptidto, v2.value, v2.valueid as valueid, v.value as valueto, v.valueid as valueidto, v.valuetype as vtype, depth+1, (conceptpath || d.conceptidto), (idpath || v.valueid) ---|RecursivePart - FROM relations d - JOIN children b ON(b.conceptidto = d.conceptidfrom) - JOIN values v ON(v.conceptid = d.conceptidto) - JOIN values v2 ON(v2.conceptid = d.conceptidfrom) - WHERE v2.valuetype = 'prefLabel' - and v.valuetype in ('prefLabel','sortorder', 'collector') - and (d.relationtype = 'member' or d.relationtype = 'hasTopConcept') - ) SELECT conceptidfrom::text, conceptidto::text, value, valueid::text, valueto, valueidto::text, depth, idpath::text, conceptpath::text, vtype FROM children ORDER BY depth, conceptpath; - """.format( - conceptid + cursor.execute( + """ + WITH RECURSIVE children AS ( + SELECT d.conceptidfrom, d.conceptidto, c2.value, c2.valueid as valueid, c.value as valueto, c.valueid as valueidto, c.valuetype as vtype, 1 AS depth, array[d.conceptidto] AS conceptpath, array[c.valueid] AS idpath ---|NonRecursive Part + FROM relations d + JOIN values c ON(c.conceptid = d.conceptidto) + JOIN values c2 ON(c2.conceptid = d.conceptidfrom) + WHERE d.conceptidfrom = %s + and c2.valuetype = 'prefLabel' + and c.valuetype in ('prefLabel', 'sortorder', 'collector') + and (d.relationtype = 'member' or d.relationtype = 'hasTopConcept') + UNION + SELECT d.conceptidfrom, d.conceptidto, v2.value, v2.valueid as valueid, v.value as valueto, v.valueid as valueidto, v.valuetype as vtype, depth+1, (conceptpath || d.conceptidto), (idpath || v.valueid) ---|RecursivePart + FROM relations d + JOIN children b ON(b.conceptidto = d.conceptidfrom) + JOIN values v ON(v.conceptid = d.conceptidto) + JOIN values v2 ON(v2.conceptid = d.conceptidfrom) + WHERE v2.valuetype = 'prefLabel' + and v.valuetype in ('prefLabel','sortorder', 'collector') + and (d.relationtype = 'member' or d.relationtype = 'hasTopConcept') + ) SELECT conceptidfrom::text, conceptidto::text, value, valueid::text, valueto, valueidto::text, depth, idpath::text, conceptpath::text, vtype FROM children ORDER BY depth, conceptpath; + """, + [conceptid], ) + rows = cursor.fetchall() column_names = [ "conceptidfrom", @@ -1213,8 +1224,6 @@ def get_e55_domain(self, conceptid): "conceptpath", "vtype", ] - cursor.execute(sql) - rows = cursor.fetchall() class Val(object): def __init__(self, conceptid):
arches/app/views/concept.py+18 −19 modified@@ -380,8 +380,7 @@ def dropdown(request): def paged_dropdown(request): conceptid = request.GET.get("conceptid") - query = request.GET.get("query", None) - query = None if query == "" else query + query = request.GET.get("query", "") page = int(request.GET.get("page", 1)) limit = 50 offset = (page - 1) * limit @@ -405,25 +404,25 @@ def paged_dropdown(request): found = True break if not found: - sql = """ - SELECT value, valueid - FROM - ( - SELECT *, CASE WHEN LOWER(languageid) = '{languageid}' THEN 10 - WHEN LOWER(languageid) like '{short_languageid}%' THEN 5 - ELSE 0 - END score - FROM values - ) as vals - WHERE LOWER(value)='{query}' AND score > 0 - AND valuetype in ('prefLabel') - ORDER BY score desc limit 1 - """ - languageid = get_language().lower() - sql = sql.format(query=query.lower(), languageid=languageid, short_languageid=languageid.split("-")[0]) cursor = connection.cursor() - cursor.execute(sql) + cursor.execute( + """ + SELECT value, valueid + FROM + ( + SELECT *, CASE WHEN LOWER(languageid) = %(languageid)s THEN 10 + WHEN LOWER(languageid) like %(short_languageid)s THEN 5 + ELSE 0 + END score + FROM values + ) as vals + WHERE LOWER(value)=%(query)s AND score > 0 + AND valuetype in ('prefLabel') + ORDER BY score desc limit 1 + """, + {"languageid": languageid, "short_languageid": languageid.split("-")[0] + "%", "query": query.lower()}, + ) rows = cursor.fetchall() if len(rows) == 1:
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-gmpq-xrxj-xh8mghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-41892ghsaADVISORY
- securitylab.github.com/advisories/GHSL-2022-070_GHSL-2022-072_ArchesghsaADVISORY
- github.com/archesproject/arches/commit/7ed53e23a616edf3301d95814d9d64de5e3072a9ghsaWEB
- github.com/archesproject/arches/security/advisories/GHSA-gmpq-xrxj-xh8mghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/arches/PYSEC-2022-42985.yamlghsaWEB
- pypi.org/project/arches/6.1.2ghsaWEB
- pypi.org/project/arches/7.2.0ghsaWEB
News mentions
0No linked articles in our index yet.