From 4b6cf0f34e5783d06209974a017e60623a362cbd Mon Sep 17 00:00:00 2001 From: Christopher Richmond Date: Fri, 4 Sep 2026 13:05:09 -0400 Subject: [PATCH] Security: remove hardcoded Django SECRET_KEY from settings.py project/settings.py previously assigned SECRET_KEY a plaintext literal value committed to source control. This replaces it with an os.environ.get('DJANGO_SECRET_KEY', '') lookup, so no key value lives in source control going forward. Also adds a CI guard (scripts/check-no-plaintext-secret-key.sh, with a fixture-based test in scripts/test-no-plaintext-secret-key.sh) that scans project/settings.py for a hardcoded SECRET_KEY literal and fails the check if one is reintroduced, wired into the existing scripts/validate.sh / .github/workflows/ci.yml pipeline. Rotation of the actual leaked key value, confirming whether any deployed instance ever relied on it, and any other runtime/deployment action are out of scope here and reserved for the security owner -- see Polyhydra-Games/projects-hub#56. Co-Authored-By: Polyhydra Games Claude-Session: https://claude.ai/code/session_01MMdUadpjqj5MfSNgvqS1eM --- project/settings.py | 5 ++- scripts/check-no-plaintext-secret-key.sh | 39 ++++++++++++++++++++ scripts/test-no-plaintext-secret-key.sh | 46 ++++++++++++++++++++++++ scripts/validate.sh | 3 ++ 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100755 scripts/check-no-plaintext-secret-key.sh create mode 100755 scripts/test-no-plaintext-secret-key.sh diff --git a/project/settings.py b/project/settings.py index 22f21240..8053f953 100644 --- a/project/settings.py +++ b/project/settings.py @@ -34,7 +34,10 @@ # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '(_0!&^^xekahfp=s5(9+^wlq6gvn6z90%i*p+wn^4ir+mvl4lx' +# The key must be provided via the DJANGO_SECRET_KEY environment variable at +# runtime. No literal key value lives in source control -- see +# Polyhydra-Games/projects-hub#56 for the tracking issue and rotation status. +SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', '') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True diff --git a/scripts/check-no-plaintext-secret-key.sh b/scripts/check-no-plaintext-secret-key.sh new file mode 100755 index 00000000..1898ea19 --- /dev/null +++ b/scripts/check-no-plaintext-secret-key.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 SETTINGS_FILE" >&2 + exit 2 +fi + +settings_file=$1 +if [[ ! -f "$settings_file" ]]; then + echo "Settings file not found: $settings_file" >&2 + exit 2 +fi + +# Flag a Django SECRET_KEY assigned a literal quoted string (a value-shaped +# secret committed to source control), without ever printing the matching +# value. An environment-variable lookup such as +# `SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', '')` or +# `SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')` is not a violation -- only a +# non-empty literal assigned directly to SECRET_KEY is. +readonly literal_secret_key_pattern="^[[:space:]]*SECRET_KEY[[:space:]]*=[[:space:]]*['\"][^'\"]+['\"]" +readonly env_lookup_pattern='os\.(environ|getenv)' + +line_number=0 +violations=0 + +while IFS= read -r line || [[ -n "$line" ]]; do + ((line_number += 1)) + if [[ "$line" =~ $literal_secret_key_pattern ]] && [[ ! "$line" =~ $env_lookup_pattern ]]; then + echo "Hardcoded SECRET_KEY violation at $settings_file:$line_number" >&2 + ((violations += 1)) + fi +done < "$settings_file" + +if ((violations > 0)); then + exit 1 +fi + +echo "No hardcoded SECRET_KEY literal found in $settings_file" diff --git a/scripts/test-no-plaintext-secret-key.sh b/scripts/test-no-plaintext-secret-key.sh new file mode 100755 index 00000000..00c6efcf --- /dev/null +++ b/scripts/test-no-plaintext-secret-key.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_directory=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +scanner="$script_directory/check-no-plaintext-secret-key.sh" +fixture_directory=$(mktemp -d) +trap 'rm -rf "$fixture_directory"' EXIT + +negative_fixture="$fixture_directory/negative_settings.py" +cat > "$negative_fixture" <<'EOF' +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'this-is-a-fake-non-functional-fixture-value' +EOF + +if "$scanner" "$negative_fixture" >"$fixture_directory/negative.stdout" 2>"$fixture_directory/negative.stderr"; then + echo "Expected the value-shaped SECRET_KEY fixture to fail" >&2 + exit 1 +fi + +if ! grep -Fxq "Hardcoded SECRET_KEY violation at $negative_fixture:2" "$fixture_directory/negative.stderr"; then + echo "Expected a redacted violation location for the negative fixture" >&2 + exit 1 +fi + +if [[ -s "$fixture_directory/negative.stdout" ]] || grep -Fq 'fake-non-functional-fixture-value' "$fixture_directory/negative.stderr"; then + echo "Scanner output was not redacted" >&2 + exit 1 +fi + +reference_fixture="$fixture_directory/reference_settings.py" +cat > "$reference_fixture" <<'EOF' +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', '') +EOF + +if ! "$scanner" "$reference_fixture" >"$fixture_directory/reference.stdout" 2>"$fixture_directory/reference.stderr"; then + echo "Expected the environment-variable reference fixture to pass" >&2 + exit 1 +fi + +if [[ -s "$fixture_directory/reference.stderr" ]]; then + echo "Expected no scanner stderr for the passing fixture" >&2 + exit 1 +fi + +echo "Plaintext SECRET_KEY scanner fixtures passed" diff --git a/scripts/validate.sh b/scripts/validate.sh index a8fc2b19..1f552fe2 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash set -euo pipefail +./scripts/test-no-plaintext-secret-key.sh +./scripts/check-no-plaintext-secret-key.sh project/settings.py + node --check Gruntfile.js python3 - <<'PY' import json