Skip to content

Harden IMPORT DATABASE source validation and require admin privilege#4422

Merged
lvca merged 1 commit into
mainfrom
security/import-database-hardening
May 30, 2026
Merged

Harden IMPORT DATABASE source validation and require admin privilege#4422
lvca merged 1 commit into
mainfrom
security/import-database-hardening

Conversation

@lvca
Copy link
Copy Markdown
Member

@lvca lvca commented May 30, 2026

Summary

Hardens the SQL IMPORT DATABASE statement and its underlying source discovery.

  • Privilege gate: IMPORT DATABASE now requires the administrative updateSecurity permission. This is a no-op in embedded mode (no security configured), so library/CLI usage is unaffected.
  • Source validation (SourceDiscovery.getSource(), the single chokepoint for both branches):
    • HTTP(S) URLs that resolve to loopback / link-local / private (site-local) / wildcard / multicast addresses are blocked by default. Controlled by arcadedb.server.security.importBlockLocalNetworks (default true). Public hosts are unaffected.
    • file:// and plain local paths can be restricted to an allow-list via arcadedb.server.security.importAllowedLocalPaths (default empty = unrestricted, backward compatible). Canonicalized to defeat .. traversal; classpath:// resources are always allowed.
  • XML importer: DTD processing and external entities are disabled (XXE / entity-expansion hardening).
  • Security violations now surface as HTTP 403.

EXPORT DATABASE / BACKUP DATABASE were reviewed and left unchanged: they already contain their output paths (reject ../separators, force the exports/backup directory).

Tests

  • ImportSecurityValidatorTest (unit, offline) - SSRF host blocking and local-path allow-list.
  • ImportDatabaseSecurityIT (server) - a non-admin user is rejected with HTTP 403.
  • DTD/entity-expansion regression test added to XMLImporterFormatTest.
  • Regression-checked: SQLLocalImporterIT, SQLRemoteImporterIT, DatabaseAdminStatementsTest, ServerImportDatabaseIT, CSVImporterIT, JsonLImporterIT.

Note

SSRF blocking defaults to on, including embedded mode; environments that legitimately import from internal hosts can set arcadedb.server.security.importBlockLocalNetworks=false.

- Require the administrative updateSecurity permission to run IMPORT DATABASE
  via SQL (no-op in embedded mode without security configured).
- Validate the import source in SourceDiscovery: optional block of HTTP(S)
  URLs that resolve to loopback/link-local/private/wildcard/multicast
  addresses (arcadedb.server.security.importBlockLocalNetworks, default true)
  and an optional local-path allow-list for file:// and plain paths
  (arcadedb.server.security.importAllowedLocalPaths, default unrestricted).
- Disable DTD processing and external entities in the XML importer.
- Surface security violations as HTTP 403.

Adds unit and integration regression tests.
@lvca lvca self-assigned this May 30, 2026
@lvca lvca added this to the 26.6.1 milestone May 30, 2026
@lvca lvca merged commit bdc414c into main May 30, 2026
9 of 13 checks passed
@lvca lvca deleted the security/import-database-hardening branch May 30, 2026 14:19
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 30, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 24 complexity

Metric Results
Complexity 24

View in Codacy

🟢 Coverage 88.46% diff coverage · -7.38% coverage variation

Metric Results
Coverage variation -7.38% coverage variation
Diff coverage 88.46% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (9945018) 128194 94278 73.54%
Head commit (e1aedc7) 159951 (+31757) 105827 (+11549) 66.16% (-7.38%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#4422) 52 46 88.46%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces security hardening for the IMPORT DATABASE command to mitigate Server-Side Request Forgery (SSRF), arbitrary local file reads, and XML External Entity (XXE) vulnerabilities. It adds configuration options to block local networks and restrict local import paths, enforces administrative privilege checks, and disables DTD processing in the XML importer. The review feedback highlights two security improvements: addressing a gap in IPv6 private address validation where Unique Local Addresses (ULA) are not blocked by isSiteLocalAddress(), and mitigating a potential DNS rebinding (TOCTOU) vulnerability by pinning the resolved IP address during connection establishment.

Comment on lines +133 to +139
static boolean isBlockedAddress(final InetAddress address) {
return address.isLoopbackAddress() // 127.0.0.0/8, ::1
|| address.isLinkLocalAddress() // 169.254.0.0/16 (cloud metadata), fe80::/10
|| address.isSiteLocalAddress() // 10/8, 172.16/12, 192.168/16
|| address.isAnyLocalAddress() // 0.0.0.0, ::
|| address.isMulticastAddress(); // 224.0.0.0/4, ff00::/8
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The isBlockedAddress method uses InetAddress.isSiteLocalAddress() to identify and block private network addresses. However, in Java, isSiteLocalAddress() only recognizes the deprecated IPv6 site-local prefix (fec0::/10) and does not return true for Unique Local Addresses (ULA, fc00::/7 / fd00::/8), which are the modern standard for private IPv6 networks. This allows an attacker to bypass the SSRF protection if the target environment uses IPv6 ULAs.\n\nTo fix this, explicitly check if the address is an Inet6Address and belongs to the fc00::/7 range.

  static boolean isBlockedAddress(final InetAddress address) {\n    if (address instanceof java.net.Inet6Address) {\n      final byte[] bytes = address.getAddress();\n      if ((bytes[0] & 0xfe) == 0xfc) {\n        return true;\n      }\n    }\n    return address.isLoopbackAddress()      // 127.0.0.0/8, ::1\n        || address.isLinkLocalAddress()     // 169.254.0.0/16 (cloud metadata), fe80::/10\n        || address.isSiteLocalAddress()     // 10/8, 172.16/12, 192.168/16\n        || address.isAnyLocalAddress()      // 0.0.0.0, ::\n        || address.isMulticastAddress();    // 224.0.0.0/4, ff00::/8\n  }

Comment on lines +59 to +90
public static void validateRemoteURL(final String url) {
if (!GlobalConfiguration.SERVER_SECURITY_IMPORT_BLOCK_LOCAL_NETWORKS.getValueAsBoolean())
return;

final int sep = url.lastIndexOf(RESOURCE_SEPARATOR);
final String urlPath = sep > -1 ? url.substring(0, sep) : url;

final String host;
try {
host = new URL(urlPath).getHost();
} catch (final MalformedURLException e) {
throw new SecurityException("IMPORT DATABASE: malformed URL");
}

if (host == null || host.isEmpty())
throw new SecurityException("IMPORT DATABASE: URL is missing a host");

final InetAddress[] addresses;
try {
addresses = InetAddress.getAllByName(host);
} catch (final UnknownHostException e) {
throw new SecurityException("IMPORT DATABASE: cannot resolve host '" + host + "'");
}

for (final InetAddress address : addresses) {
if (isBlockedAddress(address))
throw new SecurityException("IMPORT DATABASE: access to host '" + host + "' (" + address.getHostAddress()
+ ") is blocked because it resolves to a private/local network address. Set '"
+ GlobalConfiguration.SERVER_SECURITY_IMPORT_BLOCK_LOCAL_NETWORKS.getKey()
+ "=false' to allow imports from internal hosts in trusted environments");
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The validateRemoteURL method resolves the hostname to validate that it does not point to a private/local network address. However, the actual connection is established later in SourceDiscovery.getSourceFromURL using new URL(urlPath).openConnection(). This creates a Time-of-Check to Time-of-Use (TOCTOU) vulnerability, commonly known as DNS Rebinding.\n\nAn attacker can configure a malicious DNS server to return a safe public IP during the validation phase, and then return a blocked private IP (e.g., 127.0.0.1 or 169.254.169.254) when the connection is opened.\n\nTo fully mitigate this, consider pinning the resolved IP address by using a custom SocketFactory or a custom Dns resolver for the HTTP client, ensuring that the connection is made to the exact IP address that was validated.

@claude
Copy link
Copy Markdown

claude Bot commented May 30, 2026

Code Review

Solid security hardening PR. The layered approach (privilege gate + SSRF validation + local path allow-list + XXE hardening) is the right architecture, and the test coverage is good. A few observations below.


Potential Issues

DNS rebinding (SSRF residual risk)

validateRemoteURL resolves the hostname at validation time, before the actual HTTP connection is opened. An attacker with control over DNS can serve a public address during validation, then flip the TTL to a private address before the connection is established (classic DNS rebinding). This is a known limitation of pre-request DNS checks that is difficult to close without also pinning the resolved address for the subsequent HttpURLConnection. Worth a brief note in the SERVER_SECURITY_IMPORT_BLOCK_LOCAL_NETWORKS Javadoc so operators understand the residual risk.

IPv6 unique-local addresses (fc00::/7) may slip through

Java's InetAddress.isSiteLocalAddress() for IPv6 checks the old fec0::/10 prefix (deprecated by RFC 3879), not the modern fc00::/7 Unique Local Address range. An IPv6 ULA like fd00::1 would not be caught by isLoopbackAddress(), isLinkLocalAddress(), or isSiteLocalAddress(). If IPv6 environments are in scope, an explicit prefix check is needed.

Overly broad SecurityException unwrapping

for (Throwable cause = e.getTargetException(); cause != null; cause = cause.getCause())
    if (cause instanceof SecurityException se)
        throw se;

java.lang.SecurityException is also thrown by the JVM security manager for unrelated reasons (classloader restrictions, etc.). If a loaded importer implementation triggers such an exception internally, it will be incorrectly surfaced to the caller as a HTTP 403. Consider a narrower custom exception (e.g., ImportSecurityException extends SecurityException) thrown only by ImportSecurityValidator, so this catch is precise.


Minor Points

All-caps comments (// IMPORT DATABASE can create schema..., // DISABLE DTD PROCESSING..., // SURFACE A SECURITY VIOLATION...) - the project convention is lowercase comments. These stand out visually against the rest of the file.

Missing positive test for admin user - ImportDatabaseSecurityIT verifies that non-admin users are rejected (HTTP 403), but there is no assertion that an admin user can actually execute IMPORT DATABASE successfully. A single happy-path test would confirm the privilege check is not over-broad and would serve as a regression guard.

Temp file leak in localPathAllowedInsideAllowList - data.createNewFile() is not paired with a cleanup in @AfterEach or finally. The target/ directory is cleaned by Maven, so this is low severity, but worth an explicit data.deleteOnExit() for hygiene.

throws IOException on validateLocalURL signature - the method currently declares throws IOException because getCanonicalFile() can throw it, but all call sites in the production code just call it inside an already throws IOException method. No issue, just noting it's fine.


What's Done Well

  • Using InetAddress.getAllByName() (all resolved addresses) rather than getByName() closes the multi-A-record bypass.
  • Path canonicalization before allow-list comparison correctly defeats .. traversal.
  • classpath:// resources are whitelisted since they're server-bundled, not attacker-controlled.
  • DTD disabled in both load() and analyze() - the pair was easy to miss.
  • Validation placed at SourceDiscovery.getSource(), the single chokepoint, rather than scattered across callers.
  • @AfterEach reset() in ImportSecurityValidatorTest properly restores global config state, preventing test-order sensitivity.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 30, 2026

Codecov Report

❌ Patch coverage is 73.07692% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.48%. Comparing base (9945018) to head (e1aedc7).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
.../integration/importer/ImportSecurityValidator.java 69.04% 6 Missing and 7 partials ⚠️
...arcadedb/integration/importer/SourceDiscovery.java 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #4422      +/-   ##
============================================
+ Coverage     64.45%   64.48%   +0.02%     
+ Complexity      431      428       -3     
============================================
  Files          1648     1649       +1     
  Lines        128194   128245      +51     
  Branches      27477    27492      +15     
============================================
+ Hits          82630    82696      +66     
+ Misses        33948    33931      -17     
- Partials      11616    11618       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant