Enforce HTTPS for wrapper remote repositories#15809
Conversation
Reject non-HTTPS remote repository URLs before the wrapper downloads metadata or artifacts, and validate each redirect target before following it. Local filesystem repositories continue to be handled by the existing local repository path. Assisted-by: Hephaestus:openai/gpt-5.5
There was a problem hiding this comment.
Pull request overview
This PR hardens the Grails wrapper updater by enforcing HTTPS for remote repository downloads (including redirect targets), aligning wrapper-fetch behavior with audit finding requirements while keeping local filesystem repositories usable via GRAILS_REPO_URL.
Changes:
- Enforce HTTPS-only for remote wrapper repository URLs and validate redirect targets to prevent HTTP downgrade.
- Add redirect handling with an explicit redirect limit (manual redirect resolution rather than implicit follow-redirects).
- Document the HTTPS requirement for remote
GRAILS_REPO_URLentries while clarifying local filesystem support.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| INSTALL | Documents that remote GRAILS_REPO_URL repositories must be HTTPS while local filesystem repos remain supported. |
| grails-wrapper/src/test/groovy/grails/init/GrailsUpdaterSpec.groovy | Adds tests for HTTPS enforcement and redirect validation behavior. |
| grails-wrapper/src/main/java/grails/init/GrailsUpdater.java | Implements HTTPS validation and manual redirect handling with a redirect cap. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #15809 +/- ##
==================================================
+ Coverage 49.4820% 49.5119% +0.0299%
- Complexity 16698 16711 +13
==================================================
Files 1947 1947
Lines 92474 92505 +31
Branches 16152 16154 +2
==================================================
+ Hits 45758 45801 +43
+ Misses 39610 39592 -18
- Partials 7106 7112 +6
🚀 New features to boost your workflow:
|
Add direct redirect-boundary coverage for the wrapper connection helper and keep redirect error messages generic for both metadata and artifact downloads. Assisted-by: opencode:gpt-5.5
|
Addressed the Copilot feedback in follow-up commit fecd117. The missing URI import is fixed, redirect counting now has explicit boundary coverage for the allowed and rejected cases, the shared download error wording no longer says only artifact, all three review threads have replies and are marked resolved, and :grails-wrapper:check passed locally. |
✅ All tests passed ✅🏷️ Commit: fecd117 Learn more about TestLens at testlens.app. |
|
Pre-release review pass (2026-07-02, dual-reviewer: GPT-5.5 oracle + Codex CLI): APPROVE - no blocking findings. Reviewed the full diff plus local |
|
|
||
| export GRAILS_REPO_URL=$HOME/.m2/repository;https://repo1.maven.org/maven2/ | ||
|
|
||
| Remote repositories configured with `GRAILS_REPO_URL` must use HTTPS. Local file system repositories, such as `$HOME/.m2/repository`, remain supported. |
There was a problem hiding this comment.
To add this documentation, this change is incomplete - we use the GRAILS_REPO_URL in both profiles & forge. Such limits are not added there.
jdaugherty
left a comment
There was a problem hiding this comment.
- we need to make GRAILS_REPO_URL consistent (not fully implemented)
- we need to mock the server ourself or avoid external url dependencies.
| static HttpURLConnection createHttpURLConnection(String mavenMetadataFileUrl, Function<URL, HttpURLConnection> openConnection) throws IOException { | ||
| URI uri = createSecureRemoteUri(mavenMetadataFileUrl); | ||
| int redirectCount = 0; | ||
| while (true) { |
There was a problem hiding this comment.
This shouldn't be infinite. I get that MAX_REDIRECTS below is the max, but it should be used in the loop condition. I asked AI to clean this up and it came up with:
@FunctionalInterface
interface HttpURLConnectionFactory {
HttpURLConnection open(URL url) throws IOException;
}
static HttpURLConnection createHttpURLConnection(
String mavenMetadataFileUrl,
HttpURLConnectionFactory openConnection) throws IOException {
URI uri = createSecureRemoteUri(mavenMetadataFileUrl);
for (int redirectCount = 0; ; redirectCount++) {
URL url = uri.toURL();
HttpURLConnection conn = openConnection.open(url);
conn.setRequestProperty("User-Agent", "Apache-Maven/3.9.6");
conn.setInstanceFollowRedirects(false);
int responseCode = conn.getResponseCode();
if (!isRedirect(responseCode)) {
return conn; // caller owns this connection and must close/disconnect it
}
if (redirectCount >= MAX_REDIRECTS) {
closeQuietly(conn);
throw new IOException("Too many redirects while downloading Grails wrapper remote resource: "
+ mavenMetadataFileUrl);
}
String location = conn.getHeaderField("Location");
closeQuietly(conn);
if (location == null || location.isBlank()) {
throw new IOException("Redirect response from " + uri + " did not include a Location header");
}
uri = resolveSecureRedirectUrl(uri, location);
}
}
private static void closeQuietly(HttpURLConnection conn) {
try {
InputStream errorStream = conn.getErrorStream();
if (errorStream != null) {
errorStream.close();
}
} catch (IOException ignored) {
// ignore cleanup failure
} finally {
conn.disconnect();
}
}
|
|
||
| def "remote wrapper URL accepts HTTPS"() { | ||
| expect: | ||
| GrailsUpdater.createSecureRemoteUrl('https://repo.example.test/maven-metadata.xml').toString() == 'https://repo.example.test/maven-metadata.xml' |
There was a problem hiding this comment.
We should not be using external urls for the build. Use the ersatz server to set these up and test them correctly.
The Problem
The Grails Wrapper defaults to HTTPS repositories, but the repository list can be overridden via
GRAILS_REPO_URLorgrails.repo.url. Two gaps let wrapper downloads happen over an untrusted channel:http://was accepted and used for Maven metadata and wrapper artifact downloads.HttpURLConnectionwas allowed to follow redirects automatically, so an HTTPS repository could redirect to anhttp://URL and the wrapper would continue the download without re-checking the final scheme.This is a hardening issue - it requires an explicitly non-HTTPS remote repository, a hostile redirect, or network control over a non-HTTPS path - but when it happens the wrapper can fetch metadata or CLI artifacts over a tamperable channel.
The Fix
Remote wrapper repository downloads now fail closed unless the URL is HTTPS, at every hop.
HttpURLConnection.Locationtarget before use.Locationheader and redirects that downgrade HTTPS -> HTTP.Files
grails-wrapper/.../GrailsUpdater.java- HTTPS validation, manual redirect handling, downgrade/loop rejection.GrailsUpdaterSpec.groovy- new no-network boundary tests for accepted and rejected redirect chains.INSTALL- note on HTTPS enforcement.Review feedback
Copilot raised three points, all addressed: added the missing
java.net.URIimport; reworked redirect counting soMAX_REDIRECTSis a hard cap while a final non-redirect response after the allowed redirects still succeeds; and generalized redirect error wording to "wrapper remote resource" since the helper serves both metadata and artifact downloads.Testing
:grails-wrapper:test --tests "grails.init.GrailsUpdaterSpec"and:grails-wrapper:checkpass.