Skip to content

fix(#4327): use parameter binding in RemoteVertex.newEdge to prevent SQL injection#4340

Merged
robfrank merged 2 commits into
mainfrom
fix/4327-remotevertex-newedge-sql-injection
May 26, 2026
Merged

fix(#4327): use parameter binding in RemoteVertex.newEdge to prevent SQL injection#4340
robfrank merged 2 commits into
mainfrom
fix/4327-remotevertex-newedge-sql-injection

Conversation

@robfrank

Copy link
Copy Markdown
Collaborator

Closes #4327

RemoteVertex.newEdge() was building CREATE EDGE … SET prop = 'value' SQL by concatenating property values directly into the query string with no escaping. A value containing a single quote (O'Brien) caused a parse error; a malicious value could inject arbitrary SQL.

The fix replaces the string interpolation with named SQL parameters (:p0, :p1, …) and passes the values via the existing command(language, sql, Map<String,Object>) overload. The SQL template now contains only backtick-quoted property names and :pN placeholders — no user data ever appears in the query string.

Test plan

  • New RemoteVertexTest (5 tests) verifies that command() is invoked with a parameterised query and that raw property values — including apostrophes — never appear in the SQL string
  • mvn test -pl network — 373 tests, 0 failures, 0 errors
  • Both the pair-list form (newEdge(type, to, "name", "O'Brien")) and the map form (newEdge(type, to, Map.of("name", "O'Brien"))) are covered

@codacy-production

codacy-production Bot commented May 25, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Coverage 100.00% diff coverage · -7.47% coverage variation

Metric Results
Coverage variation -7.47% coverage variation
Diff coverage 100.00% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (c4cf514) 127540 93469 73.29%
Head commit (b58ba41) 159217 (+31677) 104795 (+11326) 65.82% (-7.47%)

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 (#4340) 5 5 100.00%

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.

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

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.

Code Review

This pull request addresses a SQL injection vulnerability in RemoteVertex.newEdge() by replacing direct string interpolation of property values with named SQL parameters. The feedback correctly points out that in ArcadeDB, named parameters in the query string are prefixed with a colon, but the keys in the parameter map passed to the database should not contain the leading colon. Correcting this implementation detail requires updating both the parameter generation logic in RemoteVertex and the corresponding assertions in the new unit tests.

Comment on lines +217 to +227
final String paramName = ":p" + (i / 2);

if (i > 0)
query.append(", ");

query.append("`");
query.append(propName);
query.append("` = ");
query.append(paramName);

if (propValue instanceof String)
query.append("'");
query.append(propValue);
if (propValue instanceof String)
query.append("'");
params.put(paramName, propValue);

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.

high

In ArcadeDB, named parameters in the SQL query string are prefixed with a colon (e.g., :p0), but the keys in the parameter map passed to the database should not contain the leading colon (e.g., p0). Passing keys with a leading colon in the parameter map will cause the query executor to fail to resolve the parameter values, resulting in binding null or throwing an error at runtime.\n\nTo fix this, define the parameter name without the colon, append the colon to the query string builder, and use the colon-less parameter name as the key in the params map.

        final String paramName = "p" + (i / 2);\n\n        if (i > 0)\n          query.append(", ");\n\n        query.append("`");\n        query.append(propName);\n        query.append("` = :");\n        query.append(paramName);\n\n        params.put(paramName, propValue);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The :p0 key format is correct for this codebase. NamedParameter.getValue() (engine/.../parser/NamedParameter.java:58-61) tries params.get(":" + key) first before falling back to params.get(key). So map key :p0 resolves correctly for SQL placeholder :p0.

This is also the convention used throughout the existing test suite — for example QueryTest.java uses params.put(":name", "Jay") (colon-prefixed keys). The implementation is intentional and works as-is.

assertThat(sqlCaptor.getValue()).doesNotContain("O'Brien");
assertThat(sqlCaptor.getValue()).contains(":p0");
final Map<String, Object> params = (Map<String, Object>) paramsCaptor.getValue();
assertThat(params).containsEntry(":p0", "O'Brien");

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.

high

Update the assertion to expect the parameter map key without the leading colon, matching the implementation fix.

Suggested change
assertThat(params).containsEntry(":p0", "O'Brien");
assertThat(params).containsEntry("p0", "O'Brien");

verify(mockDatabase).command(eq("sql"), sqlCaptor.capture(), paramsCaptor.capture());
assertThat(sqlCaptor.getValue()).contains(":p0");
final Map<String, Object> params = (Map<String, Object>) paramsCaptor.getValue();
assertThat(params).containsEntry(":p0", 42);

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.

high

Update the assertion to expect the parameter map key without the leading colon, matching the implementation fix.

Suggested change
assertThat(params).containsEntry(":p0", 42);
assertThat(params).containsEntry("p0", 42);

Comment on lines +116 to +117
assertThat(params).containsEntry(":p0", "O'Brien");
assertThat(params).containsEntry(":p1", 2023);

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.

high

Update the assertions to expect the parameter map keys without the leading colon, matching the implementation fix.

    assertThat(params).containsEntry("p0", "O'Brien");\n    assertThat(params).containsEntry("p1", 2023);

@claude

claude Bot commented May 25, 2026

Copy link
Copy Markdown

Code Review

The core fix is correct and addresses a real SQL injection vulnerability. Here are my findings:


Security Fix - Correct

The parameterized query approach properly eliminates the injection vector. The colon-prefixed key format (":p0") in the params map is consistent with the existing convention in the codebase (e.g., QueryTest.java uses params.put(":name", "Jay")), and NamedParameter.getValue() handles this format in its first check (params.containsKey(":" + key)).


Minor: bindFromInputParams cannot resolve colon-prefixed keys

NamedParameter.bindFromInputParams() checks params.containsKey(paramName) (without colon), then falls back to the positional index. With :p0 as the map key, the fallback params.get("0") returns null. This only affects EXPLAIN/query plan display - runtime execution is unaffected - but it is an inconsistency worth knowing about.

The existing tests in QueryTest.java use colon-prefixed keys too, so either the EXPLAIN path is already broken there or it is not exercised. No action required for this PR, but worth tracking.


Minor: Remaining injection surface not addressed in this PR

RemoteVertex.java still concatenates user-supplied strings into queries in three places:

  • countEdges() (line 57-59): edgeTypes[i] is inserted with single-quote delimiters
  • isConnectedTo(toVertex, direction, edgeType) (line 142): edgeType is inserted with single quotes
  • fetch() (line 256): types[i] is inserted with single quotes

These deal with edge-type names (schema identifiers) rather than property values, so the exploitability is lower. However, a follow-up issue to apply the same parameterization (or at least backtick-quoting for identifiers) would be a good hardening step.


Minor: Test design - indirect exception path

The tests rely on Optional.empty().get() throwing NoSuchElementException to capture the mock invocation after command() is called. This works, but it ties test correctness to the internal post-command code path. A cleaner alternative is to configure the mock to return a properly stubbed edge so the test can also verify the returned MutableEdge.


Minor: Test gaps in map-form test

newEdgeWithMapPropertiesUsesParameterBinding only asserts containsValue("it's a test") without verifying the parameter key (e.g., containsEntry(":p0", "it's a test")). The other four tests do verify specific keys. Adding that assertion here would make the map-form coverage symmetric.


Nit: Documentation file

docs/4327-remotevertex-newedge-sql-injection.md is a PR-specific analysis document. Once merged, it is orphaned from the issue tracker and contributes noise to the docs directory. The same information belongs in the commit message or the issue itself. Consider removing this file from the PR.


Nit: Raw-type suppressions

Several test methods use ArgumentCaptor<Map> (raw type) with @SuppressWarnings("unchecked"). The annotation should be @SuppressWarnings({"unchecked", "rawtypes"}) since both warnings are generated, or the code could use ArgumentCaptor.forClass(Map.class) with an explicit cast at the assertion site.


Summary

The security fix is sound. The implementation is clean and minimal. The main action items are: (1) remove the documentation file, (2) optionally strengthen the map-form test assertion, and (3) open a follow-up issue for the remaining edge-type injection surfaces.

@claude

claude Bot commented May 25, 2026

Copy link
Copy Markdown

Code Review

Summary

This PR correctly fixes a real SQL injection vulnerability in RemoteVertex.newEdge() (#4327). The approach - replacing string interpolation with named SQL parameters - is the right solution.


Positive

  • Correct fix: The :p0 key convention is confirmed correct by NamedParameter.getValue() which checks params.get(":" + key) first (line 58 of NamedParameter.java), so colon-prefixed keys work as expected.
  • Both call forms covered: The pair-list form and the map form both go through the same parameterised path.
  • Tests verify the contract: The 5 new tests confirm that apostrophes never appear in the SQL string and that values are passed as parameters.

Issues

1. Docs file should not be committed (blocking)

docs/4327-remotevertex-newedge-sql-injection.md is a PR tracking document containing internal review-cycle notes, working-tree status, and Gemini comment rebuttals. This is not project documentation - it belongs in the PR description (where it largely already exists), not committed to the repository. Please remove this file before merging.

2. Same injection pattern remains in other methods (non-blocking, worth tracking)

The fix covers newEdge(), but several other methods in the same class concatenate caller-supplied strings into SQL with single-quote wrapping - the same root cause as the original bug:

  • countEdges() (line ~56-59): edgeTypes values appended as 'value'
  • isConnectedTo(toVertex, direction, edgeType) (line ~142): edgeType appended as 'value'
  • fetch() (line ~253-257): types array elements appended as 'value'

These are lower-severity since type/edge-type names are usually developer-controlled, but the pattern is the same. Consider filing a follow-up issue.

3. No integration test against a real server (non-blocking)

The test suite mocks RemoteDatabase and relies on assertThatThrownBy (the mock returns Optional.empty() so get() throws) to capture the command() call before the exception. This verifies the parameter binding contract but does not exercise the query against an actual ArcadeDB server. A follow-up integration test would add confidence.


Minor / Style

  • params is initialised as new HashMap<>() unconditionally even when there are no properties. Moving the initialisation inside the if block would be cleaner.
  • @SuppressWarnings("unchecked") appears on every test method due to ArgumentCaptor<Map> being a raw type - acceptable, but avoidable with a typed cast pattern.
  • The assertThatThrownBy pattern is a setup mechanism (the mock returns an empty Optional). A brief inline comment explaining why the exception is expected would help future readers understand the intent.

Verdict

The security fix is correct and well-targeted. The committed docs file is the only item that should be addressed before merge.

@codecov

codecov Bot commented May 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.11%. Comparing base (c4cf514) to head (b58ba41).

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #4340      +/-   ##
============================================
- Coverage     64.21%   64.11%   -0.10%     
+ Complexity      448      422      -26     
============================================
  Files          1645     1645              
  Lines        127540   127539       -1     
  Branches      27350    27348       -2     
============================================
- Hits          81899    81777     -122     
- Misses        34102    34244     +142     
+ Partials      11539    11518      -21     

☔ 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.

@robfrank robfrank merged commit de57270 into main May 26, 2026
25 of 30 checks passed
@robfrank robfrank deleted the fix/4327-remotevertex-newedge-sql-injection branch May 26, 2026 06:35
@robfrank robfrank added this to the 26.6.1 milestone May 27, 2026
@robfrank robfrank self-assigned this May 27, 2026
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.

RemoteVertex.newEdge interpolates property values into SQL — injection + breakage on apostrophes

1 participant