fix: prevent loopback Run release URLs
This commit is contained in:
@@ -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
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user