secrets: reject runtime-reserved names (GITHUB_*, GITEA_*, FORGEJO_*, RUNNER_*, ACTIONS_*) that clobber the per-job token

9052a6b · gmorell · 2026-07-02 18:25

5 files +37 -0

Files changed

modified lib/git_gud/secrets/organization_secret.ex
+1 −0
@@ -26,6 +26,7 @@ defmodule GitGud.Secrets.OrganizationSecret do
26 26 message: "must be a valid env var name (letters, digits, _, no leading digit)"
27 27 )
28 28 |> validate_length(:name, max: 100)
29 + |> GitGud.Secrets.ReservedNames.validate_not_reserved()
29 30 |> unique_constraint(:name, name: :organization_secrets_organization_id_name_index)
30 31 end
31 32 end
modified lib/git_gud/secrets/organization_variable.ex
+1 −0
@@ -21,6 +21,7 @@ defmodule GitGud.Secrets.OrganizationVariable do
21 21 |> validate_required([:name])
22 22 |> validate_format(:name, @name_format, message: "must be a valid env var name")
23 23 |> validate_length(:name, max: 100)
24 + |> GitGud.Secrets.ReservedNames.validate_not_reserved()
24 25 |> validate_length(:value, max: 65_535)
25 26 |> unique_constraint(:name, name: :organization_variables_organization_id_name_index)
26 27 end
modified lib/git_gud/secrets/repository_secret.ex
+1 −0
@@ -26,6 +26,7 @@ defmodule GitGud.Secrets.RepositorySecret do
26 26 message: "must be a valid env var name (letters, digits, _, no leading digit)"
27 27 )
28 28 |> validate_length(:name, max: 100)
29 + |> GitGud.Secrets.ReservedNames.validate_not_reserved()
29 30 |> unique_constraint(:name, name: :repository_secrets_repository_id_name_index)
30 31 end
31 32 end
modified lib/git_gud/secrets/repository_variable.ex
+1 −0
@@ -21,6 +21,7 @@ defmodule GitGud.Secrets.RepositoryVariable do
21 21 |> validate_required([:name])
22 22 |> validate_format(:name, @name_format, message: "must be a valid env var name")
23 23 |> validate_length(:name, max: 100)
24 + |> GitGud.Secrets.ReservedNames.validate_not_reserved()
24 25 |> validate_length(:value, max: 65_535)
25 26 |> unique_constraint(:name, name: :repository_variables_repository_id_name_index)
26 27 end
added lib/git_gud/secrets/reserved_names.ex
+33 −0
@@ -0,0 +1,33 @@
1 +defmodule GitGud.Secrets.ReservedNames do
2 + @moduledoc """
3 + Rejects secret/variable names the CI runtime injects itself.
4 +
5 + A user secret named `GITHUB_TOKEN` (say) silently overrides the
6 + forge's per-job token in the runner's `secrets`/env, which then breaks
7 + `actions/checkout` on private repos ("could not read Username"). Mirror
8 + GitHub's rule (no `GITHUB_` prefix) and extend it to the Forgejo/act
9 + runtime prefixes so these can't be shadowed.
10 + """
11 + import Ecto.Changeset
12 +
13 + # Case-insensitive prefixes owned by the runtime (token, context,
14 + # runner/actions env). CI_* is intentionally NOT reserved — built-in
15 + # vars like CI_REGISTRY are meant to be user-overridable.
16 + @reserved_prefixes ~w(GITHUB_ GITEA_ FORGEJO_ RUNNER_ ACTIONS_)
17 +
18 + @doc """
19 + Add a validation error if the changeset's `field` (default `:name`)
20 + starts with a runtime-reserved prefix.
21 + """
22 + def validate_not_reserved(changeset, field \\ :name) do
23 + validate_change(changeset, field, fn ^field, value ->
24 + up = String.upcase(value || "")
25 +
26 + if Enum.any?(@reserved_prefixes, &String.starts_with?(up, &1)) do
27 + [{field, "is reserved by the CI runtime (GITHUB_*, GITEA_*, FORGEJO_*, RUNNER_*, ACTIONS_*)"}]
28 + else
29 + []
30 + end
31 + end)
32 + end
33 +end

Parents: ed9a7cd