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
View File
@@ -2,6 +2,8 @@
# Copy to .env if you want docker compose to read custom values automatically. # Copy to .env if you want docker compose to read custom values automatically.
PLATFORM_ADDR=:8080 PLATFORM_ADDR=:8080
# Public Platform URL compiled into generated Run packages. It must be reachable from the target server.
PLATFORM_RUN_RELEASE_URL=https://scum.npc0.com/
# Platform metadata storage backend: # Platform metadata storage backend:
# - file: default, writes platform metadata JSON to PLATFORM_METADATA_PATH. # - file: default, writes platform metadata JSON to PLATFORM_METADATA_PATH.
+1
View File
@@ -19,6 +19,7 @@ services:
PLATFORM_LOG_DIR: /data/platform/logs PLATFORM_LOG_DIR: /data/platform/logs
PLATFORM_ARTIFACT_DIR: /data/platform/artifacts PLATFORM_ARTIFACT_DIR: /data/platform/artifacts
PLATFORM_SECRET_ENVELOPE_KEY: ${PLATFORM_SECRET_ENVELOPE_KEY:-local-compose-secret-envelope-key-change-me} PLATFORM_SECRET_ENVELOPE_KEY: ${PLATFORM_SECRET_ENVELOPE_KEY:-local-compose-secret-envelope-key-change-me}
PLATFORM_RUN_RELEASE_URL: ${PLATFORM_RUN_RELEASE_URL:-https://scum.npc0.com/}
ports: ports:
- "8080:8080" - "8080:8080"
volumes: volumes:
+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_SOURCE_REVISION=main
PLATFORM_BUILDER_WORKSPACE_DIR=.platform-data/distribution-builds PLATFORM_BUILDER_WORKSPACE_DIR=.platform-data/distribution-builds
PLATFORM_BUILDER_TIMEOUT_SECONDS=1800 PLATFORM_BUILDER_TIMEOUT_SECONDS=1800
# URL embedded into generated Run packages; override for tunnel or production access. # URL embedded into generated Run packages. It must be reachable from the target server.
PLATFORM_RUN_RELEASE_URL=http://127.0.0.1:8080 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_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_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_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: 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 // component auth key, inside the platform. Unlike GetDistributionBuildInput it
// never crosses the job channel. // never crosses the job channel.
func (svc *CoreService) platformDistributionBuildInput(job domain.Job) (domain.DistributionBuildInput, error) { 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}) runDistributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: job.ServerInstanceID})
if err != nil { if err != nil {
return domain.DistributionBuildInput{}, err return domain.DistributionBuildInput{}, err
@@ -173,7 +177,7 @@ func (svc *CoreService) platformDistributionBuildInput(job domain.Job) (domain.D
TargetOS: distribution.TargetOS, TargetOS: distribution.TargetOS,
TargetArch: distribution.TargetArch, TargetArch: distribution.TargetArch,
TargetRelease: distribution.ID, TargetRelease: distribution.ID,
PlatformURL: runReleasePlatformURL(), PlatformURL: platformURL,
PackageFormat: distribution.PackageFormat, PackageFormat: distribution.PackageFormat,
ArtifactID: distribution.ArtifactID, ArtifactID: distribution.ArtifactID,
OutputFilename: executableFilename("run", distribution.TargetOS), 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 { if job.State != domain.JobStateAccepted && job.State != domain.JobStateRunning {
return domain.DistributionBuildInput{}, validationError("distribution build job is not active") 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}) runDistributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: job.ServerInstanceID})
if err != nil { if err != nil {
@@ -64,7 +68,7 @@ func (svc *CoreService) GetDistributionBuildInput(request domain.DistributionBui
TargetOS: distribution.TargetOS, TargetOS: distribution.TargetOS,
TargetArch: distribution.TargetArch, TargetArch: distribution.TargetArch,
TargetRelease: distribution.ID, TargetRelease: distribution.ID,
PlatformURL: runReleasePlatformURL(), PlatformURL: platformURL,
PackageFormat: distribution.PackageFormat, PackageFormat: distribution.PackageFormat,
ArtifactID: distribution.ArtifactID, ArtifactID: distribution.ArtifactID,
OutputFilename: executableFilename("run", distribution.TargetOS), OutputFilename: executableFilename("run", distribution.TargetOS),
+26 -4
View File
@@ -6,6 +6,8 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"net"
"net/url"
"os" "os"
"strings" "strings"
@@ -48,6 +50,9 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
if ready, reason := svc.distributionBuilderReadiness(); !ready { if ready, reason := svc.distributionBuilderReadiness(); !ready {
return domain.RunDistribution{}, validationError(reason) return domain.RunDistribution{}, validationError(reason)
} }
if _, err := runReleasePlatformURL(); err != nil {
return domain.RunDistribution{}, err
}
key, _, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "") key, _, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
if err != nil { if err != nil {
@@ -778,11 +783,28 @@ func packageFormatForTarget(targetOS string) string {
return "tar.gz" return "tar.gz"
} }
func runReleasePlatformURL() string { const defaultRunReleasePlatformURL = "https://scum.npc0.com/"
if value := strings.TrimSpace(os.Getenv("PLATFORM_RUN_RELEASE_URL")); value != "" {
return value 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 { func distributionID(prefix string, parts ...interface{}) string {
+88
View File
@@ -23,6 +23,94 @@ type generatedPackageConfig struct {
AuthKey string 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) { func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t) svc, session, instance := newDistributionTestFixture(t)
+1 -1
View File
@@ -25,7 +25,7 @@ export PLATFORM_BOOTSTRAP_ADMIN_EMAIL="${PLATFORM_BOOTSTRAP_ADMIN_EMAIL:-operato
export PLATFORM_BOOTSTRAP_ADMIN_PASSWORD="${PLATFORM_BOOTSTRAP_ADMIN_PASSWORD:-operator-local}" export PLATFORM_BOOTSTRAP_ADMIN_PASSWORD="${PLATFORM_BOOTSTRAP_ADMIN_PASSWORD:-operator-local}"
export PLATFORM_SECRET_ENVELOPE_KEY="${PLATFORM_SECRET_ENVELOPE_KEY:-local-debug-secret-envelope-key-change-me}" export PLATFORM_SECRET_ENVELOPE_KEY="${PLATFORM_SECRET_ENVELOPE_KEY:-local-debug-secret-envelope-key-change-me}"
export PLATFORM_AI_PROVIDER_MODE="${PLATFORM_AI_PROVIDER_MODE:-mock}" export PLATFORM_AI_PROVIDER_MODE="${PLATFORM_AI_PROVIDER_MODE:-mock}"
export PLATFORM_RUN_RELEASE_URL="${PLATFORM_RUN_RELEASE_URL:-http://127.0.0.1:$LOCAL_DEBUG_PLATFORM_PORT}" export PLATFORM_RUN_RELEASE_URL="${PLATFORM_RUN_RELEASE_URL:-https://scum.npc0.com/}"
export RUN_SOURCE_DIR="${RUN_SOURCE_DIR:-${RUN_REPO_DIR:-$LOCAL_DEBUG_ROOT_DIR/run}}" export RUN_SOURCE_DIR="${RUN_SOURCE_DIR:-${RUN_REPO_DIR:-$LOCAL_DEBUG_ROOT_DIR/run}}"
export RUN_REPO_DIR="$RUN_SOURCE_DIR" export RUN_REPO_DIR="$RUN_SOURCE_DIR"
+2 -2
View File
@@ -1571,8 +1571,8 @@ if [[ "$VITE_PLATFORM_API_BASE_URL" != "/api/v1" ]]; then
printf 'VITE_PLATFORM_API_BASE_URL must be /api/v1, got %s\n' "$VITE_PLATFORM_API_BASE_URL" >&2 printf 'VITE_PLATFORM_API_BASE_URL must be /api/v1, got %s\n' "$VITE_PLATFORM_API_BASE_URL" >&2
exit 1 exit 1
fi fi
if [[ "$PLATFORM_RUN_RELEASE_URL" != "$PLATFORM_URL" ]]; then if [[ "$PLATFORM_RUN_RELEASE_URL" == "$PLATFORM_URL" ]]; then
printf 'PLATFORM_RUN_RELEASE_URL must be %s for executable local Run proof, got %s\n' "$PLATFORM_URL" "$PLATFORM_RUN_RELEASE_URL" >&2 printf 'PLATFORM_RUN_RELEASE_URL must use an address reachable from the generated Run target, not the local platform URL %s\n' "$PLATFORM_URL" >&2
exit 1 exit 1
fi fi
if [[ "$PLATFORM_API_PROXY" != "$PLATFORM_URL" ]]; then if [[ "$PLATFORM_API_PROXY" != "$PLATFORM_URL" ]]; then