-
Notifications
You must be signed in to change notification settings - Fork 560
Fix URL signing api for URLs containing special characters #12435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 5 commits
f911647
2c94dc8
96be125
8812582
9cb852a
12216ba
942f3cf
4387ee9
8b1ab7b
4bbb576
4e8b6ff
77ba0c8
9a4d41f
96ec148
5706157
ddcf8ff
06f539e
faa7064
2e0749f
60c1832
0e60702
ff6d335
94f71c0
3fb4cc1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| ### Signed URLs work again for URLs with special characters | ||
|
|
||
| Requesting a signed URL (e.g. via `/api/admin/requestSignedUrl`, used by external tools, the Globus | ||
| integration and third-party integrations) was broken for URLs whose query contained special | ||
| characters; most notably persistent IDs such as `doi:10.5072/FK2/ABC` (which contain `:` and | ||
| `/`), as well as spaces, percent-encoded values and non-ASCII characters. The signing logic had | ||
| started normalizing/re-encoding the URL before signing it, while the signature is a byte-exact MAC | ||
| over the URL string; the re-encoded bytes no longer matched what callers presented back, so | ||
| validation failed with "signature does not match"/authentication errors. | ||
|
|
||
| Signing is now byte-exact again: the base URL is preserved exactly as provided (reserved signing | ||
| parameters are still stripped, but without re-encoding the rest of the URL). Clients must continue to | ||
| present the signed URL exactly as Dataverse returned it. | ||
|
|
||
| ### A signing secret is now required to request signed URLs | ||
|
|
||
| The `/api/admin/requestSignedUrl` endpoint now requires a non-empty signing secret | ||
| (`dataverse.api.signing-secret`) to be configured. Previously an unset secret silently fell back to | ||
| using only the user's API token as the signing key, which is too weak. If the secret is not | ||
| configured, the endpoint now returns an error instead of issuing a weakly-signed URL. | ||
|
|
||
| **Upgrade note:** installations that use signed URLs through this endpoint (including the | ||
| `rdm-integration` connector) must set `dataverse.api.signing-secret`. See the | ||
| [Configuration Guide](https://guides.dataverse.org/en/latest/installation/config.html#dataverse-api-signing-secret). | ||
| Treat the value like a password. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2453,7 +2453,13 @@ public Response getSignedUrl(@Context ContainerRequestContext crc, JsonObject ur | |
| if (superuser == null || !superuser.isSuperuser()) { | ||
| return error(Response.Status.FORBIDDEN, "Requesting signed URLs is restricted to superusers."); | ||
| } | ||
|
|
||
|
|
||
| // Require a signing secret: without it the key is only the user's API token, which is too weak. | ||
| if (JvmSettings.API_SIGNING_SECRET.lookupOptional().orElse("").isEmpty()) { | ||
| return error(Response.Status.BAD_REQUEST, | ||
| "Requesting signed URLs requires a signing secret to be configured. Please set the dataverse.api.signing-secret JVM option."); | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My concern with this being here is that it doesn't cover the URL signing when requesting a file download with a guestbook response. Those APIs still work without the secret. (See Access.java) These APIs all call UrlSignerUtil.signUrl, which should have this code to return an error if no signing-secret exists. {
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. Two commits: first I added the same guard to the guestbook download ( I kept it out of Heads up: centralizing means all such signing now needs the secret: external tool/Globus callbacks and permission-history links too, not just the two endpoints. Every install using them must set That may be too much; happy to scope it back to just |
||
| String userId = urlInfo.getString("user"); | ||
| String key=null; | ||
| if (userId != null) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,12 +2,10 @@ | |
|
|
||
| import org.apache.commons.codec.digest.DigestUtils; | ||
| import org.apache.http.NameValuePair; | ||
| import org.apache.http.client.utils.URIBuilder; | ||
| import org.apache.http.client.utils.URLEncodedUtils; | ||
| import org.joda.time.LocalDateTime; | ||
|
|
||
| import java.net.MalformedURLException; | ||
| import java.net.URISyntaxException; | ||
| import java.net.URL; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.List; | ||
|
|
@@ -45,19 +43,10 @@ public class UrlSignerUtil { | |
| */ | ||
| public static String signUrl(String baseUrl, Integer timeout, String user, String method, String key) { | ||
|
|
||
| // check for reserved parameter names ("until","user", "method", or "token") | ||
| String[] urlQP = baseUrl.split("\\?"); | ||
| if (urlQP.length > 1) { | ||
| try { | ||
| URIBuilder uriBuilder = new URIBuilder(baseUrl); | ||
| List<NameValuePair> params = uriBuilder.getQueryParams(); | ||
| params.removeIf(pair -> reservedParameters.contains(pair.getName())); | ||
| uriBuilder.setParameters(params); | ||
| baseUrl = uriBuilder.build().toString(); | ||
| } catch (URISyntaxException e) { | ||
| logger.severe("Invalid URL for signing: " + baseUrl + " " + e.getMessage()); | ||
| } | ||
| } | ||
| // Strip reserved signing params that may already be in the base URL. Done with exact-string | ||
| // surgery (not URIBuilder): the signature is a byte-exact MAC, so re-encoding the URL here | ||
| // (e.g. percent-encoding ':' and '/' in DOIs) would change the hashed bytes and break it. | ||
| baseUrl = stripReservedParameters(baseUrl); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for working with the existing code. FWIW: I think it would be cleaner overall for this general utility to not try fixing a caller's mistakes - perhaps it should throw an exception if one of the four params needed by the algorithm are sent (and never touch the url on the good path and thus not needing to strip anything). Similarly flagging/removing signed and key here because the caller doesn't strip them seems backwards.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This wasn't introduced in this PR. It came in with the 6.10 guestbook changes (#12001), where the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 2e0749f: |
||
| boolean firstParam = !baseUrl.contains("?"); | ||
| StringBuilder signedUrlBuilder = new StringBuilder(baseUrl); | ||
|
|
||
|
|
@@ -87,6 +76,32 @@ public static String signUrl(String baseUrl, Integer timeout, String user, Strin | |
| return signedUrl; | ||
| } | ||
|
|
||
| /** | ||
| * Removes the reserved signing parameters from the query, preserving the exact bytes of the path | ||
| * and of every other parameter (unlike URIBuilder, which would re-encode and break the MAC). | ||
| */ | ||
| static String stripReservedParameters(String baseUrl) { | ||
| int queryStart = baseUrl.indexOf('?'); | ||
| if (queryStart < 0) { | ||
| return baseUrl; | ||
| } | ||
| String path = baseUrl.substring(0, queryStart); | ||
| String query = baseUrl.substring(queryStart + 1); | ||
| StringBuilder kept = new StringBuilder(); | ||
| for (String pair : query.split("&")) { | ||
| int equals = pair.indexOf('='); | ||
| String name = (equals < 0) ? pair : pair.substring(0, equals); | ||
| if (reservedParameters.contains(name)) { | ||
| continue; | ||
| } | ||
| if (kept.length() > 0) { | ||
| kept.append('&'); | ||
| } | ||
| kept.append(pair); | ||
| } | ||
| return kept.length() > 0 ? path + "?" + kept : path; | ||
| } | ||
|
|
||
| /** | ||
| * This method will only return true if the URL and parameters except the | ||
| * "token" are unchanged from the original/match the values sent to this method, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.