From 2157d1a66da8ddf9400f93a36fc58acb037e8831 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 13 Oct 2025 13:01:33 +0000 Subject: [PATCH] ingress: Sanitize env vars --- custom-domain/dstack-ingress/README.md | 2 +- .../dstack-ingress/scripts/entrypoint.sh | 19 +++++ .../dstack-ingress/scripts/functions.sh | 70 +++++++++++++++++++ .../scripts/tests/test_sanitizers.sh | 70 +++++++++++++++++++ 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 custom-domain/dstack-ingress/scripts/functions.sh create mode 100644 custom-domain/dstack-ingress/scripts/tests/test_sanitizers.sh diff --git a/custom-domain/dstack-ingress/README.md b/custom-domain/dstack-ingress/README.md index b900dd5..0de4cd0 100644 --- a/custom-domain/dstack-ingress/README.md +++ b/custom-domain/dstack-ingress/README.md @@ -176,7 +176,7 @@ configs: - `CERTBOT_EMAIL`: Your email address used in Let's Encrypt certificate requests - `TARGET_ENDPOINT`: The plain HTTP endpoint of your dstack application (for single domain mode) - `SET_CAA`: Set to `true` to enable CAA record setup -- `CLIENT_MAX_BODY_SIZE`: Optional value for nginx `client_max_body_size` (e.g. `50m`) in single-domain mode +- `CLIENT_MAX_BODY_SIZE`: Optional value for nginx `client_max_body_size` (numeric with optional `k|m|g` suffix, e.g. `50m`) in single-domain mode **Backward Compatibility:** diff --git a/custom-domain/dstack-ingress/scripts/entrypoint.sh b/custom-domain/dstack-ingress/scripts/entrypoint.sh index a3b5a3a..a150687 100644 --- a/custom-domain/dstack-ingress/scripts/entrypoint.sh +++ b/custom-domain/dstack-ingress/scripts/entrypoint.sh @@ -2,8 +2,27 @@ set -e +source "/scripts/functions.sh" + PORT=${PORT:-443} TXT_PREFIX=${TXT_PREFIX:-"_dstack-app-address"} + +if ! PORT=$(sanitize_port "$PORT"); then + exit 1 +fi +if ! DOMAIN=$(sanitize_domain "$DOMAIN"); then + exit 1 +fi +if ! TARGET_ENDPOINT=$(sanitize_target_endpoint "$TARGET_ENDPOINT"); then + exit 1 +fi +if ! CLIENT_MAX_BODY_SIZE=$(sanitize_client_max_body_size "$CLIENT_MAX_BODY_SIZE"); then + exit 1 +fi +if ! TXT_PREFIX=$(sanitize_dns_label "$TXT_PREFIX"); then + exit 1 +fi + PROXY_CMD="proxy" if [[ "${TARGET_ENDPOINT}" == grpc://* ]]; then PROXY_CMD="grpc" diff --git a/custom-domain/dstack-ingress/scripts/functions.sh b/custom-domain/dstack-ingress/scripts/functions.sh new file mode 100644 index 0000000..a945b45 --- /dev/null +++ b/custom-domain/dstack-ingress/scripts/functions.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +# Sanitizer helpers shared across scripts. Each function echoes the sanitized +# value on success; on failure it writes an error to stderr and returns non-zero. + +sanitize_port() { + local candidate="$1" + if [[ "$candidate" =~ ^[0-9]+$ ]] && (( candidate >= 1 && candidate <= 65535 )); then + echo "$candidate" + else + echo "Error: Invalid PORT value: $candidate" >&2 + return 1 + fi +} + +sanitize_domain() { + local candidate="$1" + if [ -z "$candidate" ]; then + echo "" + return 0 + fi + if [[ "$candidate" =~ ^(\*\.)?[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$ ]]; then + echo "$candidate" + else + echo "Error: Invalid DOMAIN value: $candidate" >&2 + return 1 + fi +} + +sanitize_target_endpoint() { + local candidate="$1" + if [ -z "$candidate" ]; then + echo "" + return 0 + fi + if [[ "$candidate" =~ ^(grpc|https?)://[A-Za-z0-9._-]+(:[0-9]{1,5})?(/[A-Za-z0-9._~:/?&=%-]*)?$ ]]; then + echo "$candidate" + else + echo "Error: Invalid TARGET_ENDPOINT value: $candidate" >&2 + return 1 + fi +} + +sanitize_client_max_body_size() { + local candidate="$1" + if [ -z "$candidate" ]; then + echo "" + return 0 + fi + if [[ "$candidate" =~ ^[0-9]+[kKmMgG]?$ ]]; then + echo "$candidate" + else + echo "Warning: Ignoring invalid CLIENT_MAX_BODY_SIZE value: $candidate" >&2 + echo "" + fi +} + +sanitize_dns_label() { + local candidate="$1" + if [ -z "$candidate" ]; then + echo "Error: TXT_PREFIX cannot be empty" >&2 + return 1 + fi + if [[ "$candidate" =~ ^[A-Za-z0-9_-]+$ ]]; then + echo "$candidate" + else + echo "Error: Invalid TXT_PREFIX value: $candidate" >&2 + return 1 + fi +} diff --git a/custom-domain/dstack-ingress/scripts/tests/test_sanitizers.sh b/custom-domain/dstack-ingress/scripts/tests/test_sanitizers.sh new file mode 100644 index 0000000..be8e66f --- /dev/null +++ b/custom-domain/dstack-ingress/scripts/tests/test_sanitizers.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +set -euo pipefail + +THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPTS_DIR="$(cd "${THIS_DIR}/.." && pwd)" + +# shellcheck source=../functions.sh +source "${SCRIPTS_DIR}/functions.sh" + +failures=0 + +assert_equal() { + local actual="$1" + local expected="$2" + local msg="$3" + if [[ "$actual" != "$expected" ]]; then + echo "FAIL: ${msg} (expected '${expected}', got '${actual}')" >&2 + failures=$((failures + 1)) + else + echo "PASS: ${msg}" + fi +} + +assert_fails() { + local msg="$1" + shift + local output_file + output_file="$(mktemp)" + if "$@" >"$output_file" 2>&1; then + echo "FAIL: ${msg} (expected failure)" >&2 + cat "$output_file" >&2 + failures=$((failures + 1)) + else + cat "$output_file" + echo "PASS: ${msg}" + fi + rm -f "$output_file" +} + +# Successful cases +assert_equal "$(sanitize_port 8080)" "8080" "sanitize_port accepts numeric port" +assert_equal "$(sanitize_domain example.com)" "example.com" "sanitize_domain accepts fqdn" +assert_equal "$(sanitize_domain '*.example.com')" "*.example.com" "sanitize_domain accepts wildcard" +assert_equal "$(sanitize_target_endpoint http://service:80/path)" "http://service:80/path" "sanitize_target_endpoint accepts http" +assert_equal "$(sanitize_target_endpoint grpc://svc:50051)" "grpc://svc:50051" "sanitize_target_endpoint accepts grpc" +assert_equal "$(sanitize_client_max_body_size 50m)" "50m" "sanitize_client_max_body_size accepts suffix" +assert_equal "$(sanitize_dns_label test_label)" "test_label" "sanitize_dns_label accepts lowercase" +assert_equal "$(sanitize_dns_label test-label)" "test-label" "sanitize_dns_label accepts hyphen" + +# Failing cases +assert_fails "sanitize_port rejects non-numeric" sanitize_port abc +assert_fails "sanitize_domain rejects invalid domain" sanitize_domain bad_domain +assert_fails "sanitize_target_endpoint rejects malformed URL" sanitize_target_endpoint "http:///broken" +warning_output="$(sanitize_client_max_body_size "50mb" 2>&1 || true)" +if [[ "$warning_output" == "Warning: Ignoring invalid CLIENT_MAX_BODY_SIZE value: 50mb" ]]; then + echo "PASS: sanitize_client_max_body_size warns and returns empty" +else + echo "FAIL: sanitize_client_max_body_size warning unexpected" + printf '%s\n' "$warning_output" + failures=$((failures + 1)) +fi +assert_fails "sanitize_dns_label rejects invalid characters" sanitize_dns_label "bad*label" + +if [[ $failures -eq 0 ]]; then + echo "All sanitizer tests passed" +else + echo "$failures sanitizer tests failed" >&2 + exit 1 +fi