fix: prevent loopback Run release URLs

This commit is contained in:
npc0-hue
2026-09-05 18:11:58 +08:00
parent 4923308fb4
commit cf836715e2
10 changed files with 133 additions and 12 deletions
+2 -2
View File
@@ -37,5 +37,5 @@ PLATFORM_BUILDER_SOURCE_REPOSITORY=git@git.npc0.com:admin343/run.git
PLATFORM_BUILDER_SOURCE_REVISION=main
PLATFORM_BUILDER_WORKSPACE_DIR=.platform-data/distribution-builds
PLATFORM_BUILDER_TIMEOUT_SECONDS=1800
# URL embedded into generated Run packages; override for tunnel or production access.
PLATFORM_RUN_RELEASE_URL=http://127.0.0.1:8080
# URL embedded into generated Run packages. It must be reachable from the target server.
PLATFORM_RUN_RELEASE_URL=https://scum.npc0.com/
+1 -1
View File
@@ -62,7 +62,7 @@ Runtime configuration:
- `PLATFORM_BUILDER_WORKSPACE_DIR`: private per-plugin/per-job build workspace, default `<PLATFORM_DATA_DIR>/distribution-builds`.
- `PLATFORM_BUILDER_CACHE_DIR`: persistent Go build/module cache, default `<PLATFORM_DATA_DIR>/distribution-build-cache`; it contains no job inputs or component keys.
- `PLATFORM_BUILDER_TIMEOUT_SECONDS`: positive build deadline, default `1800`.
- `PLATFORM_RUN_RELEASE_URL`: public platform URL embedded into generated Run packages, default `http://127.0.0.1:8080/`; set this explicitly for tunnel or production access.
- `PLATFORM_RUN_RELEASE_URL`: public platform URL embedded into generated Run packages, default `https://scum.npc0.com/`. It must be reachable from the target server; loopback and unspecified addresses are rejected.
Build the dedicated toolchain image before enabling distribution generation:
@@ -140,6 +140,10 @@ func (svc *CoreService) executeDistributionBuild(job domain.Job) error {
// component auth key, inside the platform. Unlike GetDistributionBuildInput it
// never crosses the job channel.
func (svc *CoreService) platformDistributionBuildInput(job domain.Job) (domain.DistributionBuildInput, error) {
platformURL, err := runReleasePlatformURL()
if err != nil {
return domain.DistributionBuildInput{}, err
}
runDistributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: job.ServerInstanceID})
if err != nil {
return domain.DistributionBuildInput{}, err
@@ -173,7 +177,7 @@ func (svc *CoreService) platformDistributionBuildInput(job domain.Job) (domain.D
TargetOS: distribution.TargetOS,
TargetArch: distribution.TargetArch,
TargetRelease: distribution.ID,
PlatformURL: runReleasePlatformURL(),
PlatformURL: platformURL,
PackageFormat: distribution.PackageFormat,
ArtifactID: distribution.ArtifactID,
OutputFilename: executableFilename("run", distribution.TargetOS),
+5 -1
View File
@@ -30,6 +30,10 @@ func (svc *CoreService) GetDistributionBuildInput(request domain.DistributionBui
if job.State != domain.JobStateAccepted && job.State != domain.JobStateRunning {
return domain.DistributionBuildInput{}, validationError("distribution build job is not active")
}
platformURL, err := runReleasePlatformURL()
if err != nil {
return domain.DistributionBuildInput{}, err
}
runDistributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: job.ServerInstanceID})
if err != nil {
@@ -64,7 +68,7 @@ func (svc *CoreService) GetDistributionBuildInput(request domain.DistributionBui
TargetOS: distribution.TargetOS,
TargetArch: distribution.TargetArch,
TargetRelease: distribution.ID,
PlatformURL: runReleasePlatformURL(),
PlatformURL: platformURL,
PackageFormat: distribution.PackageFormat,
ArtifactID: distribution.ArtifactID,
OutputFilename: executableFilename("run", distribution.TargetOS),
+26 -4
View File
@@ -6,6 +6,8 @@ import (
"encoding/hex"
"errors"
"fmt"
"net"
"net/url"
"os"
"strings"
@@ -48,6 +50,9 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
if ready, reason := svc.distributionBuilderReadiness(); !ready {
return domain.RunDistribution{}, validationError(reason)
}
if _, err := runReleasePlatformURL(); err != nil {
return domain.RunDistribution{}, err
}
key, _, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
if err != nil {
@@ -778,11 +783,28 @@ func packageFormatForTarget(targetOS string) string {
return "tar.gz"
}
func runReleasePlatformURL() string {
if value := strings.TrimSpace(os.Getenv("PLATFORM_RUN_RELEASE_URL")); value != "" {
return value
const defaultRunReleasePlatformURL = "https://scum.npc0.com/"
func runReleasePlatformURL() (string, error) {
value := strings.TrimSpace(os.Getenv("PLATFORM_RUN_RELEASE_URL"))
if value == "" {
value = defaultRunReleasePlatformURL
}
return "http://127.0.0.1:8080/"
parsed, err := url.ParseRequestURI(value)
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.ForceQuery || parsed.Fragment != "" || strings.Contains(value, "#") {
return "", validationError("PLATFORM_RUN_RELEASE_URL must be an absolute http or https URL without credentials, query parameters, or fragments")
}
host := strings.TrimRight(strings.ToLower(parsed.Hostname()), ".")
if host == "" {
return "", validationError("PLATFORM_RUN_RELEASE_URL must be an absolute http or https URL with a host")
}
if host == "localhost" || strings.HasSuffix(host, ".localhost") {
return "", validationError("PLATFORM_RUN_RELEASE_URL must use an address reachable from the target server; loopback or unspecified addresses are not valid for generated Run packages")
}
if ip := net.ParseIP(host); ip != nil && (ip.IsLoopback() || ip.IsUnspecified()) {
return "", validationError("PLATFORM_RUN_RELEASE_URL must use an address reachable from the target server; loopback or unspecified addresses are not valid for generated Run packages")
}
return value, nil
}
func distributionID(prefix string, parts ...interface{}) string {
+88
View File
@@ -23,6 +23,94 @@ type generatedPackageConfig struct {
AuthKey string
}
func TestRunReleasePlatformURLUsesExternalDefault(t *testing.T) {
t.Setenv("PLATFORM_RUN_RELEASE_URL", "")
value, err := runReleasePlatformURL()
if err != nil {
t.Fatalf("resolve default Run release URL: %v", err)
}
if value != defaultRunReleasePlatformURL {
t.Fatalf("unexpected default Run release URL %q", value)
}
}
func TestRunReleasePlatformURLRejectsTargetLocalAddresses(t *testing.T) {
for _, value := range []string{
"http://127.0.0.1:18080",
"http://127.0.0.2:18080",
"http://localhost:18080",
"https://platform.localhost/",
"http://[::1]:18080",
"http://0.0.0.0:18080",
"http://[::]:18080",
} {
t.Run(value, func(t *testing.T) {
t.Setenv("PLATFORM_RUN_RELEASE_URL", value)
if _, err := runReleasePlatformURL(); err == nil || !strings.Contains(err.Error(), "reachable from the target server") {
t.Fatalf("expected target-local URL %q to be rejected, got %v", value, err)
}
})
}
}
func TestRunReleasePlatformURLAcceptsExternalHTTPURL(t *testing.T) {
t.Setenv("PLATFORM_RUN_RELEASE_URL", "http://platform.example.test:8080/control")
value, err := runReleasePlatformURL()
if err != nil {
t.Fatalf("resolve external Run release URL: %v", err)
}
if value != "http://platform.example.test:8080/control" {
t.Fatalf("unexpected external Run release URL %q", value)
}
}
func TestRunReleasePlatformURLRejectsInvalidURLForms(t *testing.T) {
for _, value := range []string{
"ftp://platform.example.test",
"https://operator@platform.example.test",
"https://platform.example.test/?token=value",
"https://platform.example.test/?",
"https://platform.example.test/#fragment",
"https://platform.example.test/#",
} {
t.Run(value, func(t *testing.T) {
t.Setenv("PLATFORM_RUN_RELEASE_URL", value)
if _, err := runReleasePlatformURL(); err == nil || !strings.Contains(err.Error(), "absolute http or https URL") {
t.Fatalf("expected invalid Run release URL %q to be rejected, got %v", value, err)
}
})
}
}
func TestCoreServiceRejectsLoopbackRunReleaseURLBeforeGeneration(t *testing.T) {
t.Setenv("PLATFORM_RUN_RELEASE_URL", "http://127.0.0.1:18080")
svc, session, instance := newDistributionTestFixture(t)
_, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "windows",
TargetArch: "amd64",
IdempotencyKey: "loopback-release-url",
})
if err == nil || !strings.Contains(err.Error(), "reachable from the target server") {
t.Fatalf("expected loopback Run release URL to fail generation, got %v", err)
}
distributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: instance.ID})
if err != nil || len(distributions) != 0 {
t.Fatalf("invalid release URL must not create distributions, distributions=%+v err=%v", distributions, err)
}
keys, err := svc.store.EncryptedComponentKeys().List(domain.EncryptedComponentKeyFilter{ServerInstanceID: instance.ID, ComponentKind: domain.DistributionComponentRun})
if err != nil || len(keys) != 0 {
t.Fatalf("invalid release URL must not create component keys, keys=%+v err=%v", keys, err)
}
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
if err != nil || len(jobs) != 0 {
t.Fatalf("invalid release URL must not create build jobs, jobs=%+v err=%v", jobs, err)
}
}
func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)