From f79ed989866c246a04e5911f247afd10b6b09f1c Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Fri, 5 Sep 2025 09:34:35 +0200 Subject: [PATCH] encode user-provided URL parts (#109) In various methods we use user-provided values like role names or lease ids as parts of the API request path. Apply URL encoding to these paths that are not expected to contain any path separators or query args. --- CHANGELOG.md | 1 + .../jvault/connector/HTTPVaultConnector.java | 43 ++++++++++--------- .../connector/internal/RequestHelper.java | 18 ++++++-- .../connector/internal/VaultApiPath.java | 7 +-- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 221368b..883e5de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Improvements * Extract API paths into a utility class (#108) +* Encode user-provided URL parts (#109) ### Fix * Prevent potential off-by-1 error in internal `mapOf()` helper (#107) diff --git a/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java b/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java index 33c23e8..77d4634 100644 --- a/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java +++ b/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import static de.stklcode.jvault.connector.internal.RequestHelper.encode; import static de.stklcode.jvault.connector.internal.VaultApiPath.*; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; @@ -170,7 +171,7 @@ public class HTTPVaultConnector implements VaultConnector { public final AuthResponse authUserPass(final String username, final String password) throws VaultConnectorException { final Map payload = singletonMap("password", password); - return queryAuth(AUTH_USERPASS_LOGIN + username, payload); + return queryAuth(AUTH_USERPASS_LOGIN + encode(username), payload); } @Override @@ -179,7 +180,7 @@ public class HTTPVaultConnector implements VaultConnector { "role_id", roleID, "secret_id", secretID ); - return queryAuth(AUTH_APPROLE + LOGIN, payload); + return queryAuth(AUTH_APPROLE + "login", payload); } /** @@ -207,7 +208,7 @@ public class HTTPVaultConnector implements VaultConnector { requireAuth(); /* Issue request and expect code 204 with empty response */ - request.postWithoutResponse(String.format(AUTH_APPROLE_ROLE, role.getName(), ""), role, token); + request.postWithoutResponse(AUTH_APPROLE_ROLE + encode(role.getName()), role, token); /* Set custom ID if provided */ return !(role.getId() != null && !role.getId().isEmpty()) || setAppRoleID(role.getName(), role.getId()); @@ -218,7 +219,7 @@ public class HTTPVaultConnector implements VaultConnector { requireAuth(); /* Request HTTP response and parse Secret */ return request.get( - String.format(AUTH_APPROLE_ROLE, roleName, ""), + AUTH_APPROLE_ROLE + encode(roleName), emptyMap(), token, AppRoleResponse.class @@ -230,7 +231,7 @@ public class HTTPVaultConnector implements VaultConnector { requireAuth(); /* Issue request and expect code 204 with empty response */ - request.deleteWithoutResponse(String.format(AUTH_APPROLE_ROLE, roleName, ""), token); + request.deleteWithoutResponse(AUTH_APPROLE_ROLE + encode(roleName), token); return true; } @@ -240,7 +241,7 @@ public class HTTPVaultConnector implements VaultConnector { requireAuth(); /* Issue request, parse response and extract Role ID */ return request.get( - String.format(AUTH_APPROLE_ROLE, roleName, "/role-id"), + AUTH_APPROLE_ROLE + encode(roleName) + "/role-id", emptyMap(), token, RawDataResponse.class @@ -253,7 +254,7 @@ public class HTTPVaultConnector implements VaultConnector { /* Issue request and expect code 204 with empty response */ request.postWithoutResponse( - String.format(AUTH_APPROLE_ROLE, roleName, "/role-id"), + AUTH_APPROLE_ROLE + encode(roleName) + "/role-id", singletonMap("role_id", roleID), token ); @@ -268,14 +269,14 @@ public class HTTPVaultConnector implements VaultConnector { if (secret.getId() != null && !secret.getId().isEmpty()) { return request.post( - String.format(AUTH_APPROLE_ROLE, roleName, "/custom-secret-id"), + AUTH_APPROLE_ROLE + encode(roleName) + "/custom-secret-id", secret, token, AppRoleSecretResponse.class ); } else { return request.post( - String.format(AUTH_APPROLE_ROLE, roleName, "/secret-id"), + AUTH_APPROLE_ROLE + encode(roleName) + "/secret-id", secret, token, AppRoleSecretResponse.class ); @@ -289,7 +290,7 @@ public class HTTPVaultConnector implements VaultConnector { /* Issue request and parse secret response */ return request.post( - String.format(AUTH_APPROLE_ROLE, roleName, "/secret-id/lookup"), + AUTH_APPROLE_ROLE + encode(roleName) + "/secret-id/lookup", new AppRoleSecret(secretID), token, AppRoleSecretResponse.class @@ -303,7 +304,7 @@ public class HTTPVaultConnector implements VaultConnector { /* Issue request and expect code 204 with empty response */ request.postWithoutResponse( - String.format(AUTH_APPROLE_ROLE, roleName, "/secret-id/destroy"), + AUTH_APPROLE_ROLE + encode(roleName) + "/secret-id/destroy", new AppRoleSecret(secretID), token); @@ -315,7 +316,7 @@ public class HTTPVaultConnector implements VaultConnector { requireAuth(); SecretListResponse secrets = request.get( - AUTH_APPROLE + "/role?list=true", + AUTH_APPROLE + "role?list=true", emptyMap(), token, SecretListResponse.class @@ -329,7 +330,7 @@ public class HTTPVaultConnector implements VaultConnector { requireAuth(); SecretListResponse secrets = request.get( - String.format(AUTH_APPROLE_ROLE, roleName, "/secret-id?list=true"), + AUTH_APPROLE_ROLE + encode(roleName) + "/secret-id?list=true", emptyMap(), token, SecretListResponse.class @@ -502,7 +503,7 @@ public class HTTPVaultConnector implements VaultConnector { requireAuth(); /* Issue request and expect code 204 with empty response */ - request.putWithoutResponse(SYS_LEASES_REVOKE + leaseID, emptyMap(), token); + request.putWithoutResponse(SYS_LEASES_REVOKE + encode(leaseID), emptyMap(), token); } @Override @@ -533,7 +534,7 @@ public class HTTPVaultConnector implements VaultConnector { if (role == null || role.isEmpty()) { throw new InvalidRequestException("No role name specified."); } - return createTokenInternal(token, AUTH_TOKEN + TOKEN_CREATE + "/" + role); + return createTokenInternal(token, AUTH_TOKEN + TOKEN_CREATE + "/" + encode(role)); } @Override @@ -586,7 +587,7 @@ public class HTTPVaultConnector implements VaultConnector { } // Issue request and expect code 204 with empty response. - request.postWithoutResponse(AUTH_TOKEN + TOKEN_ROLES + "/" + name, role, token); + request.postWithoutResponse(AUTH_TOKEN + TOKEN_ROLES + "/" + encode(name), role, token); return true; } @@ -596,7 +597,7 @@ public class HTTPVaultConnector implements VaultConnector { requireAuth(); // Request HTTP response and parse response. - return request.get(AUTH_TOKEN + TOKEN_ROLES + "/" + name, emptyMap(), token, TokenRoleResponse.class); + return request.get(AUTH_TOKEN + TOKEN_ROLES + "/" + encode(name), emptyMap(), token, TokenRoleResponse.class); } @Override @@ -615,7 +616,7 @@ public class HTTPVaultConnector implements VaultConnector { } // Issue request and expect code 204 with empty response. - request.deleteWithoutResponse(AUTH_TOKEN + TOKEN_ROLES + "/" + name, token); + request.deleteWithoutResponse(AUTH_TOKEN + TOKEN_ROLES + "/" + encode(name), token); return true; } @@ -629,7 +630,7 @@ public class HTTPVaultConnector implements VaultConnector { "plaintext", plaintext ); - return request.post(TRANSIT_ENCRYPT + keyName, payload, token, TransitResponse.class); + return request.post(TRANSIT_ENCRYPT + encode(keyName), payload, token, TransitResponse.class); } @Override @@ -641,7 +642,7 @@ public class HTTPVaultConnector implements VaultConnector { "ciphertext", ciphertext ); - return request.post(TRANSIT_DECRYPT + keyName, payload, token, TransitResponse.class); + return request.post(TRANSIT_DECRYPT + encode(keyName), payload, token, TransitResponse.class); } @Override @@ -658,7 +659,7 @@ public class HTTPVaultConnector implements VaultConnector { "format", format ); - return request.post(TRANSIT_HASH + algorithm, payload, token, TransitResponse.class); + return request.post(TRANSIT_HASH + encode(algorithm), payload, token, TransitResponse.class); } /** diff --git a/src/main/java/de/stklcode/jvault/connector/internal/RequestHelper.java b/src/main/java/de/stklcode/jvault/connector/internal/RequestHelper.java index b49942a..81141b7 100644 --- a/src/main/java/de/stklcode/jvault/connector/internal/RequestHelper.java +++ b/src/main/java/de/stklcode/jvault/connector/internal/RequestHelper.java @@ -25,6 +25,7 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.Duration; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CompletionException; import java.util.stream.Collectors; @@ -263,9 +264,9 @@ public final class RequestHelper implements Serializable { if (!payload.isEmpty()) { uriBuilder.append("?").append( - payload.entrySet().stream().map(par -> - URLEncoder.encode(par.getKey(), UTF_8) + "=" + URLEncoder.encode(par.getValue(), UTF_8) - ).collect(Collectors.joining("&")) + payload.entrySet().stream() + .map(par -> encode(par.getKey()) + "=" + encode(par.getValue())) + .collect(Collectors.joining("&")) ); } @@ -307,6 +308,17 @@ public final class RequestHelper implements Serializable { } } + /** + * Encode URL part. + * + * @param part Path part to URL-encode and insert into the template + * @return Encoded URL part + * @since 1.5.3 + */ + public static String encode(final String part) { + return URLEncoder.encode(Objects.requireNonNullElse(part, ""), UTF_8); + } + /** * Execute prepared HTTP request and return result. * diff --git a/src/main/java/de/stklcode/jvault/connector/internal/VaultApiPath.java b/src/main/java/de/stklcode/jvault/connector/internal/VaultApiPath.java index ae05d07..f2b60de 100644 --- a/src/main/java/de/stklcode/jvault/connector/internal/VaultApiPath.java +++ b/src/main/java/de/stklcode/jvault/connector/internal/VaultApiPath.java @@ -40,8 +40,8 @@ public final class VaultApiPath { // Auth paths public static final String AUTH_TOKEN = AUTH + "/token"; public static final String AUTH_USERPASS_LOGIN = AUTH + "/userpass/login/"; - public static final String AUTH_APPROLE = AUTH + "/approle"; - public static final String AUTH_APPROLE_ROLE = AUTH_APPROLE + "/role/%s%s"; + public static final String AUTH_APPROLE = AUTH + "/approle/"; + public static final String AUTH_APPROLE_ROLE = AUTH_APPROLE + "role/"; // Token operations public static final String TOKEN_LOOKUP = "/lookup"; @@ -57,9 +57,6 @@ public final class VaultApiPath { public static final String SECRET_UNDELETE = "/undelete/"; public static final String SECRET_DESTROY = "/destroy/"; - // Generic paths - public static final String LOGIN = "/login"; - // Transit engine paths public static final String TRANSIT_ENCRYPT = TRANSIT + "/encrypt/"; public static final String TRANSIT_DECRYPT = TRANSIT + "/decrypt/";