fix(#4793): gRPC getDatabase rejects path-traversal names and enforces auth/authorization#4815
Conversation
|
Tick the box to add this pull request to the merge queue (same as
|
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Coverage variation | ✅ -7.26% coverage variation |
| Diff coverage | ✅ 76.67% diff coverage |
Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (85768bf) 133181 98777 74.17% Head commit (44b40be) 165012 (+31831) 110402 (+11625) 66.91% (-7.26%) 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 (#4815) 30 23 76.67% 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.
There was a problem hiding this comment.
Code Review
This pull request addresses a security vulnerability (Issue #4793) in ArcadeDbGrpcService where unauthenticated users could perform path traversal or unauthorized database creation/access via gRPC. It introduces database name validation to prevent path traversal and enforces proper authentication and per-database authorization. The feedback points out a security concern where a database name of exactly "." is not rejected, which could resolve to the root databases directory, and suggests explicitly rejecting it.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if (databaseName.contains("/") || databaseName.contains("\\") || databaseName.contains("..")) | ||
| throw new IllegalArgumentException("Invalid database name: " + databaseName); |
There was a problem hiding this comment.
The current validation checks for .., /, and \, but does not reject a database name of exactly .. If a client requests a database named ., the resolved path becomes databasePath + "/.", which points directly to the root databases directory. This allows a client to potentially open, modify, or drop the entire databases directory as if it were a single database. To prevent this directory escape, explicitly reject . as an invalid database name.
| if (databaseName.contains("/") || databaseName.contains("\\") || databaseName.contains("..")) | |
| throw new IllegalArgumentException("Invalid database name: " + databaseName); | |
| if (databaseName.equals(".") || databaseName.contains("/") || databaseName.contains("\\") || databaseName.contains("..")) | |
| throw new IllegalArgumentException("Invalid database name: " + databaseName); |
There was a problem hiding this comment.
Good catch - fixed in c425c28. A bare "." resolves to the databases directory itself, so validateDatabaseName now rejects "." alongside the separator and parent-reference checks, with a regression case added to Issue4793GrpcGetDatabaseSecurityIT.
|
Review: PR 4815 - gRPC Thanks for this - it closes a genuine HIGH-severity gap and the change is well scoped, well documented, and centralized in the one method ( What is good
Issues / suggestions
Minor
Overall: solid, targeted security fix. Item 1 is the one I would most encourage addressing before merge since it affects the client-visible API contract; the rest are cleanups. |
Review: gRPC
|
|
Thanks for the thorough review. Addressed in 4e5a009:
|
Review: PR #4815 — gRPC
|
…s auth/authorization ArcadeDbGrpcService.getDatabase resolved a request-supplied database name after only checking that a username was resolvable. Two security problems: - Path traversal: the name flowed unsanitized into new DatabaseFactory(databasePath + "/" + databaseName) and arcadeServer.getDatabase(databaseName); a name containing "..", "/" or "\\" escaped the configured databases directory. - Weak auth: validateCredentials never authenticated the password nor authorized the user for the named database. Fix: - New validateDatabaseName rejects blank names and names containing "/", "\\" or ".." before any filesystem access (mirrors PostServerCommandHandler). - validateCredentials now takes the database name and, when server security is active, authorizes the interceptor-authenticated context user for the requested database, or authenticates request-payload username/password via ServerSecurity.authenticate. Adds Issue4793GrpcGetDatabaseSecurityIT (3 of 5 cases fail before the fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address gemini-code-assist review: a database name of exactly "." resolves to the databases directory itself (databasePath + "/."), letting a caller target the whole databases directory. validateDatabaseName now rejects "." alongside the separator and parent-reference checks. Adds a regression case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address claude review: - validateDatabaseName throws INVALID_ARGUMENT; authn failures UNAUTHENTICATED; per-database authorization failures PERMISSION_DENIED. executeQuery preserves the StatusRuntimeException instead of masking it as INTERNAL, so clients see the right code. Tests now assert Status.Code.INVALID_ARGUMENT to pin the contract. - Fix stale Javadoc reference on resolvedUsername to the new validateCredentials(DatabaseCredentials, String) signature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4e5a009 to
44b40be
Compare
Review: fix(#4793) gRPC getDatabase path-traversal + auth/authorizationThanks for this - the fix is well-targeted and the core security improvement is solid: Suggestions1. Reuse final Set<String> allowedDatabases = user.getAuthorizedDatabases();
if (!allowedDatabases.contains(SecurityManager.ANY) && !allowedDatabases.contains(databaseName))
throw Status.PERMISSION_DENIED...
2. Test coverage gap on the actual cross-database escape this PR closes. 3. The request-payload authentication branch (the 4. Status-code preservation is only wired into Minor
Nice work on the IT design (real gRPC channel, interceptor, and the on-disk |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4815 +/- ##
=========================================
Coverage 65.18% 65.18%
Complexity 715 715
=========================================
Files 1680 1680
Lines 133181 133209 +28
Branches 28467 28475 +8
=========================================
+ Hits 86811 86832 +21
+ Misses 34441 34438 -3
- Partials 11929 11939 +10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The per-database bypass described in #4794 is already closed by validateCredentials(credentials, databaseName), which getDatabase() invokes as its first step (added in #4815). That method authorizes the resolved user against the request-body database the operation actually targets: interceptor -context users are checked against getAuthorizedDatabases(), and request -payload credentials go through ServerSecurity.authenticate(user, pass, databaseName). Both throw PERMISSION_DENIED ("User has not access to database '<db>'") before any database work happens. An earlier revision of this branch added a second canAccessToDatabase() gate in getDatabase(); since validateCredentials runs first and checks the same condition on the same database name, that gate was unreachable dead code with a divergent message. Drop it (and a duplicate import) and keep only the valuable contribution: Issue4794GrpcPerDbAuthorizationIT, which exercises the header-database != body-database scenarios end-to-end and asserts the denial raised by validateCredentials.
The per-database bypass described in #4794 is already closed by validateCredentials(credentials, databaseName), which getDatabase() invokes as its first step (added in #4815). That method authorizes the resolved user against the request-body database the operation actually targets: interceptor -context users are checked against getAuthorizedDatabases(), and request -payload credentials go through ServerSecurity.authenticate(user, pass, databaseName). Both throw PERMISSION_DENIED ("User has not access to database '<db>'") before any database work happens. An earlier revision of this branch added a second canAccessToDatabase() gate in getDatabase(); since validateCredentials runs first and checks the same condition on the same database name, that gate was unreachable dead code with a divergent message. Drop it (and a duplicate import) and keep only the valuable contribution: Issue4794GrpcPerDbAuthorizationIT, which exercises the header-database != body-database scenarios end-to-end and asserts the denial raised by validateCredentials.
The per-database bypass described in #4794 is already closed by validateCredentials(credentials, databaseName), which getDatabase() invokes as its first step (added in #4815). That method authorizes the resolved user against the request-body database the operation actually targets: interceptor -context users are checked against getAuthorizedDatabases(), and request -payload credentials go through ServerSecurity.authenticate(user, pass, databaseName). Both throw PERMISSION_DENIED ("User has not access to database '<db>'") before any database work happens. An earlier revision of this branch added a second canAccessToDatabase() gate in getDatabase(); since validateCredentials runs first and checks the same condition on the same database name, that gate was unreachable dead code with a divergent message. Drop it (and a duplicate import) and keep only the valuable contribution: Issue4794GrpcPerDbAuthorizationIT, which exercises the header-database != body-database scenarios end-to-end and asserts the denial raised by validateCredentials.
Issue
Fixes #4793 (HIGH severity, security).
ArcadeDbGrpcService.getDatabaseresolved a request-supplieddatabaseNameafter only checking that a username was resolvable.validateCredentialsnever calledServerSecurity.authenticate. Two problems:databaseNameflowed unsanitized intonew DatabaseFactory(databasePath + "/" + databaseName)(fallback path) and intoarcadeServer.getDatabase(databaseName). A name containing..,/or\escapes the configured databases directory, letting a caller open/create databases anywhere on disk.Fix
grpcw/.../ArcadeDbGrpcService.java:validateDatabaseName(String)(new): rejects null/blank names and any name containing/,\or.., before any filesystem access. Mirrors the established pattern inPostServerCommandHandler.resolveBackupFile. Called at the top ofgetDatabase.validateCredentialsnow takes the database name and, when server security is active (at least one user configured), enforces real authentication + per-database authorization:ServerSecurity.authenticate.securityEnabledgate.Tests
grpcw/.../Issue4793GrpcGetDatabaseSecurityIT.java(new): parent-dir.., embedded/, and\database names are rejected (and no directory escapes the databases folder); blank name rejected; a legitimate authorized access still works. 3 of the 5 cases fail before the fix, all pass after.Verification
grpcwunit tests (104, incl.ArcadeDbGrpcServiceExtendedTest) +GrpcAuthInterceptorTest(8): pass.grpcwITsGrpcServerIT+GrpcFollowerForwardingIT(31, incl. HA leader-forwarding via the interceptor context user): pass.grpc-client Issue4562RollbackDeleteTest(HTTP-path schema-cache) verified to fail identically on the unmodified base - not caused by this change.🤖 Generated with Claude Code