Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
🚧 Files skipped from review as they are similar to previous changes (6)
📝 WalkthroughWalkthroughThe change adds template-controlled task survey-variable validation, persists the ChangesTask survey variable handling
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant TaskController
participant Task
participant Template
participant TaskPool
TaskController->>Task: ValidateSurveyVars(template)
Task->>Template: Resolve and validate survey variables
Template-->>Task: Validation result
Task-->>TaskController: Success or error
TaskController->>TaskPool: AddTask(task)
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Stale comment
Security review
Outcome: No medium/high/critical vulnerabilities found in the added or modified code.
This PR is a security hardening change. It adds backend validation (
ValidateSurveyVars) on the primary task-creation API path (POST /project/{id}/tasks) to block undeclared survey variables — including Ansible connection overrides such asansible_ssh_common_argsthat can execute code on the Semaphore server.What was reviewed
- Input → sink tracing:
Task.Environment/Task.Secretare validated againsttemplate.survey_varsbeforeTaskPool.AddTaskruns on the user-facing endpoint.- Controls verified: Undeclared keys are rejected by default; secret survey vars must use
secret; int/enum types are checked;AllowAnyVarsInTaskis opt-in and defaults tofalse.- Bypass paths checked: Integrations, schedules, and autorun child tasks call
TaskPool.AddTaskdirectly without this validation, but those paths only accept environment keys configured by project managers (integration extract mappings / schedule task params), not arbitrary keys from task runners. That is a different trust boundary than the vulnerability this PR fixes.- Other classes: No new injection surfaces, authz bypasses, secret leakage, or dependency changes were introduced.
Residual note (not reported as a finding)
Placing
ValidateSurveyVarsin the HTTP handler instead ofTaskPool.AddTaskmeans future task-creation entry points must remember to call it. Consider centralizing enforcement inTaskPool.AddTaskfor defense in depth.
Automated security review
Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 75ea78cafd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // undeclared keys change settings the template author never exposed — see | ||
| // db.Task.ValidateSurveyVars. Templates whose tasks legitimately need | ||
| // arbitrary variables opt in with AllowAnyVarsInTask. | ||
| if err = taskObj.ValidateSurveyVars(tpl); err != nil { |
There was a problem hiding this comment.
Enforce survey validation in the shared task path
When a task is created by an integration or schedule, api/integration.go:366 and services/schedules/SchedulePool.go:164 call TaskPool.AddTask directly, bypassing this controller-only check. Consequently, a webhook extractor can still place request-controlled, undeclared or type-invalid values into the task environment despite the new default restriction, including the connection options this change is intended to block. Apply the validation in the shared task creation path before the secret is cleared.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Summary
- Moved survey-variable validation into the shared
TaskPool.AddTaskpath, ensuring API, integration, and scheduled tasks all receive identical validation. Secret variables are validated before the secret payload is cleared. services/tasks/TaskPool.goL1023-L1047 - Preserved the REST endpoint’s existing client-facing behavior by returning validation failures through
helpers.WriteErrorrather than converting them into internal-server errors. api/projects/tasks.goL51-L78 - Committed the changes as
a848e9e(fix(tasks): validate survey vars for every task source) and created the follow-up pull request metadata titled “fix(tasks): enforce survey validation across task sources.”
Testing
- ✅
git diff --check - ✅
go test ./db ./services/tasks ./api/projects - ✅
git status --short --branch(working tree clean)
| // declare. It is disabled by default because such variables override the | ||
| // template environment and are passed to the app with the highest | ||
| // precedence — see Task.ValidateSurveyVars. | ||
| AllowAnyVarsInTask bool `db:"allow_any_vars_in_task" json:"allow_any_vars_in_task,omitempty"` |
There was a problem hiding this comment.
Return the opt-in flag from template list queries
With the SQL store, GetTemplates and GetTemplatesWithPermissions use the explicit field list in db/sql/template.go:264-288, which omits pt.allow_any_vars_in_task. A template that has this flag enabled is therefore serialized as if it were disabled by the project/view template-list endpoints; clients that round-trip a listed template can also unintentionally clear the setting on update. Add the new column to that select list.
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| return nil |
There was a problem hiding this comment.
Reject structured values for text survey variables
For SurveyVarStr and SurveyVarText, every non-null, non-empty value falls through to this unconditional success, so payloads such as {"app_version":{"unexpected":true}} pass validation even though the survey declares a text input. Secret values have the same problem because the secret loop only checks the variable name and never validates its value. These structured values then reach the application as objects or are stringified for env-target variables; require JSON strings for the string, text, and secret types.
Useful? React with 👍 / 👎.
| case SurveyVarEnum: | ||
| for _, allowed := range v.Values { | ||
| if fmt.Sprintf("%v", value) == allowed.Value { | ||
| return nil | ||
| } |
There was a problem hiding this comment.
Reject non-string enum inputs instead of coercing them
When an enum contains the string value "1" or "true", a caller can submit the JSON number 1 or boolean true and this fmt.Sprintf comparison accepts it. The original non-string value is then passed to the application, violating the declared enum type and potentially changing application behavior. Check that the input is a string before comparing it with the allowed enum values.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR adds backend enforcement for template “survey variable” constraints so that, by default, tasks can only submit variables declared in a template’s survey_vars, with a new per-template opt-out (allow_any_vars_in_task) for advanced use cases.
Changes:
- Adds
allow_any_vars_in_tasktemplate flag (UI + DB + API docs) to optionally permit undeclared task variables. - Implements backend validation (
Task.ValidateSurveyVars) to reject undeclared variables and enforce declared types (with support for “secret” survey vars viaTask.Secret). - Adds SQL migration + tests covering both the new validation behavior and migration persistence.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| web/src/lang/en.js | Adds i18n strings for the new template checkbox and hint text. |
| web/src/components/TemplateForm.vue | Adds UI control to toggle allow_any_vars_in_task on templates. |
| web/src/components/TaskForm.vue | Filters undeclared vars on re-run unless the template allows arbitrary vars. |
| db/Template.go | Introduces SurveyVarSecret, fixes survey var const typing, and adds value validation helpers + AllowAnyVarsInTask field. |
| db/Task.go | Adds parsing + validation of task-provided environment/secret vars against template survey definitions. |
| db/Task_test.go | Adds unit tests for ValidateSurveyVars and GetSurveyVar. |
| db/sql/template.go | Persists allow_any_vars_in_task on create/update in SQL store. |
| db/sql/migrations/v2.20.2.sql | Adds allow_any_vars_in_task column (default false). |
| db/sql/migrations/v2.20.2.err.sql | Down-migration for the new column. |
| db/sql/migration_2_20_2_test.go | Verifies migration adds the column and SQL store persists the flag. |
| db/Migration.go | Registers migration version 2.20.2. |
| api/projects/tasks.go | Enforces backend survey var validation when creating tasks via API. |
| api-docs.yml | Documents the new template flag and task environment/secret validation semantics. |
| // By default a task may carry only the variables declared in the template | ||
| // survey, no matter who sends it: task variables override the template | ||
| // environment and are passed to the app with the highest precedence, so | ||
| // undeclared keys change settings the template author never exposed — see | ||
| // db.Task.ValidateSurveyVars. Templates whose tasks legitimately need | ||
| // arbitrary variables opt in with AllowAnyVarsInTask. | ||
| if err = taskObj.ValidateSurveyVars(tpl); err != nil { |
| dec := json.NewDecoder(strings.NewReader(payload)) | ||
| dec.UseNumber() | ||
|
|
||
| if err := dec.Decode(&res); err != nil { | ||
| return nil, common_errors.NewValidationError("task " + field + " must be a JSON object") | ||
| } | ||
|
|
||
| return res, nil |
| <v-checkbox | ||
| class="mt-0" | ||
| v-model="item.allow_any_vars_in_task" | ||
| :label="$t('allow_any_vars_in_task')" | ||
| :hint="$t('allow_any_vars_in_task_hint')" | ||
| persistent-hint | ||
| /> |
| switch v.Type { | ||
| case SurveyVarInt: | ||
| switch val := value.(type) { | ||
| case json.Number: | ||
| if _, err := val.Int64(); err == nil { | ||
| return nil | ||
| } | ||
| case string: | ||
| if _, err := strconv.ParseInt(val, 10, 64); err == nil { | ||
| return nil | ||
| } | ||
| } | ||
| return common_errors.NewValidationError("survey variable " + v.Name + " must be an integer") | ||
| case SurveyVarEnum: | ||
| for _, allowed := range v.Values { | ||
| if fmt.Sprintf("%v", value) == allowed.Value { | ||
| return nil | ||
| } | ||
| } | ||
| return common_errors.NewValidationError("survey variable " + v.Name + " has a value which is not allowed") | ||
| } | ||
|
|
||
| return nil | ||
| } |
| for name := range secrets { | ||
| v := template.GetSurveyVar(name) | ||
|
|
||
| if v == nil || v.Type != SurveyVarSecret { | ||
| return undeclaredSurveyVarError(name) | ||
| } | ||
| } |
| "suppress_success_alerts, app, git_branch, runner_tag, task_params, "+ | ||
| "allow_override_branch_in_task, allow_parallel_tasks, jwt_params)"+ | ||
| "allow_override_branch_in_task, allow_parallel_tasks, allow_any_vars_in_task, "+ | ||
| "jwt_params)"+ |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@api/projects/tasks.go`:
- Around line 51-61: Move the ValidateSurveyVars check from the direct REST
handler into TaskPool.AddTask, using the task’s associated template before
accepting any task. Ensure all AddTask callers, including schedules and
integrations, enforce the same validation while preserving the existing error
propagation behavior.
In `@db/Task.go`:
- Around line 201-219: Update parseTaskVars to verify that no non-whitespace
data remains after the initial JSON object is decoded. Use the existing decoder
dec and reject trailing content, returning the same validation error for invalid
Task.Environment or Task.Secret payloads while preserving valid object parsing.
In `@db/Template.go`:
- Around line 130-165: Update Task.ValidateSurveyVars to validate every declared
SurveyVar, not only supplied values, and return a validation error when a
variable with Required set is absent or null. Preserve ValidateValue’s
optional-value behavior for non-required variables, while ensuring required
omissions fail during task creation/validation before execution.
In `@web/src/components/TaskForm.vue`:
- Around line 332-334: Update the rerun initialization near editedEnvironment
and editedSecretEnvironment so the parsed v.secret object also passes through
filterDeclaredVars before assignment. Preserve the existing JSON fallback and
ensure editedSecretEnvironment contains only declared variables, preventing
beforeSave from serializing undeclared secrets.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a4695007-0f76-44c8-ab2f-b1e2b7b2e710
📒 Files selected for processing (13)
api-docs.ymlapi/projects/tasks.godb/Migration.godb/Task.godb/Task_test.godb/Template.godb/sql/migration_2_20_2_test.godb/sql/migrations/v2.20.2.err.sqldb/sql/migrations/v2.20.2.sqldb/sql/template.goweb/src/components/TaskForm.vueweb/src/components/TemplateForm.vueweb/src/lang/en.js
| // By default a task may carry only the variables declared in the template | ||
| // survey, no matter who sends it: task variables override the template | ||
| // environment and are passed to the app with the highest precedence, so | ||
| // undeclared keys change settings the template author never exposed — see | ||
| // db.Task.ValidateSurveyVars. Templates whose tasks legitimately need | ||
| // arbitrary variables opt in with AllowAnyVarsInTask. | ||
| if err = taskObj.ValidateSurveyVars(tpl); err != nil { | ||
| helpers.WriteError(w, err) | ||
| return | ||
| } | ||
|
|
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find other places that build/queue db.Task and check whether they call ValidateSurveyVars
rg -n --type=go 'AddTask\(' --glob '!*_test.go'
rg -n --type=go 'ValidateSurveyVars' --glob '!*_test.go'Repository: semaphoreui/semaphore
Length of output: 159
🏁 Script executed:
#!/bin/bash
set +e
printf 'tracked files:\n'
git ls-files | sed -n '1,120p'
printf '\nFind tasks.go:\n'
fd -a 'tasks\.go$' . 2>/dev/null || true
printf '\nSearch AddTask ValidateSurveyVars (no glob):\n'
rg -n 'AddTask\(|ValidateSurveyVars' -g '*.go' . || true
printf '\nSearch Schedule/Integration/Workflow task creation terms:\n'
rg -n 'schedule|Schedule|integration|Integration|workflow|Workflow|Environment:|Env:|TaskId|Task' -g '*.go' . | sed -n '1,240p'Repository: semaphoreui/semaphore
Length of output: 25986
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- api/projects/tasks.go ---\n'
cat -n api/projects/tasks.go | sed -n '1,130p'
printf '\n--- services/tasks/TaskPool.go AddTask section ---\n'
cat -n services/tasks/TaskPool.go | sed -n '960,1055p'
printf '\n--- services/schedules/SchedulePool.go task creation section ---\n'
cat -n services/schedules/SchedulePool.go | sed -n '130,190p'
printf '\n--- api/integration.go task creation section ---\n'
cat -n api/integration.go | sed -n '320,380p'
printf '\n--- Pro workflow interfaces and call sites ---\n'
cat -n pro_interfaces/workflow_svc.go | sed -n '1,80p'
rg -n --type=go 'AddTask\(' services api pro* -g '*.go' || true
rg -n --type=go 'ValidateSurveyVars' services api db -g '*.go' || trueRepository: semaphoreui/semaphore
Length of output: 250
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- api/projects/tasks.go ---'
cat -n api/projects/tasks.go | sed -n '1,130p'
printf '%s\n' ''
printf '%s\n' '--- services/tasks/TaskPool.go AddTask section ---'
cat -n services/tasks/TaskPool.go | sed -n '960,1055p'
printf '%s\n' ''
printf '%s\n' '--- services/schedules/SchedulePool.go task creation section ---'
cat -n services/schedules/SchedulePool.go | sed -n '130,190p'
printf '%s\n' ''
printf '%s\n' '--- api/integration.go task creation section ---'
cat -n api/integration.go | sed -n '320,380p'
printf '%s\n' ''
printf '%s\n' '--- Pro workflow interfaces and call sites ---'
cat -n pro_interfaces/workflow_svc.go | sed -n '1,80p'
rg -n --type=go 'AddTask\(' services api pro* -g '*.go' || true
rg -n --type=go 'ValidateSurveyVars' services api db -g '*.go' || trueRepository: semaphoreui/semaphore
Length of output: 18511
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- db/Task.go relevant validations ---'
cat -n db/Task.go | sed -n '200,295p'
printf '%s\n' ''
printf '%s\n' '--- db/TaskParams and schedule task creation ---'
cat -n db/TaskParams.go | sed -n '1,220p'
cat -n db/Schedule.go | sed -n '1,140p'
printf '%s\n' ''
printf '%s\n' '--- api/integration.go GetTaskDefinition ---'
cat -n api/integration.go | sed -n '240,320p'
printf '%s\n' ''
printf '%s\n' '--- TaskPool validation remainder ---'
cat -n services/tasks/TaskPool.go | sed -n '1028,1080p'
printf '%s\n' ''
printf '%s\n' '--- validateSurveyVars implementation/test cases ---'
cat -n db/Task.go | sed -n '220,360p'
cat -n db/Task_test.go | sed -n '1,170p'Repository: semaphoreui/semaphore
Length of output: 21739
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- db/Task.go ValidateNewTask ---'
rg -n -A80 -B10 'func \(task \*Task\) ValidateNewTask' db/Task.go
cat -n db/Task.go | sed -n '140,205p'
printf '%s\n' ''
printf '%s\n' '--- api integration task creation after GetTaskDefinition ---'
cat -n api/integration.go | sed -n '286,350p'
printf '%s\n' ''
printf '%s\n' '--- all AddTask call sites with enough context ---'
for f in api/projects/tasks.go services/schedules/SchedulePool.go api/integration.go; do
printf '%s\n' 'FILE: '"$f"
rg -n -C3 'AddTask\(' "$f" --glob '*.go' || true
doneRepository: semaphoreui/semaphore
Length of output: 8366
Enforce survey-var validation at the task-pool entry point.
ValidateSurveyVars is only checked by the direct REST task handler before calling TaskPool.AddTask; TaskPool.AddTask only calls ValidateNewTask, which does not validate environment/secret survey variables. Since schedules and integrations also pass tasks through TaskPool.AddTask, any Environment or Secret supplied by webhooks/schedules can bypass this validation. Move the validation into TaskPool.AddTask or validate the same way in every AddTask call site.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@api/projects/tasks.go` around lines 51 - 61, Move the ValidateSurveyVars
check from the direct REST handler into TaskPool.AddTask, using the task’s
associated template before accepting any task. Ensure all AddTask callers,
including schedules and integrations, enforce the same validation while
preserving the existing error propagation behavior.
| // parseTaskVars parses a task-supplied variable payload (Task.Environment or | ||
| // Task.Secret) into a map. Numbers are decoded as json.Number so integer survey | ||
| // variables can be checked without float rounding. | ||
| func parseTaskVars(payload string, field string) (map[string]any, error) { | ||
| res := make(map[string]any) | ||
|
|
||
| if payload == "" { | ||
| return res, nil | ||
| } | ||
|
|
||
| dec := json.NewDecoder(strings.NewReader(payload)) | ||
| dec.UseNumber() | ||
|
|
||
| if err := dec.Decode(&res); err != nil { | ||
| return nil, common_errors.NewValidationError("task " + field + " must be a JSON object") | ||
| } | ||
|
|
||
| return res, nil | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Reject trailing data in parseTaskVars.
json.Decoder.Decode accepts {"app_version":"1.2.3"}garbage without error, only consuming the first object. Since this parser is meant to tighten parsing of Task.Environment / Task.Secret, reject payloads that contain non-whitespace data after the JSON object using dec.More().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@db/Task.go` around lines 201 - 219, Update parseTaskVars to verify that no
non-whitespace data remains after the initial JSON object is decoded. Use the
existing decoder dec and reject trailing content, returning the same validation
error for invalid Task.Environment or Task.Secret payloads while preserving
valid object parsing.
| // ValidateValue checks a task-supplied value against the type declared for the | ||
| // variable. An absent value (null or empty string) is accepted: the variable is | ||
| // simply left unset, and required-ness is not enforced here. | ||
| func (v *SurveyVar) ValidateValue(value any) error { | ||
| if value == nil { | ||
| return nil | ||
| } | ||
|
|
||
| if s, ok := value.(string); ok && s == "" { | ||
| return nil | ||
| } | ||
|
|
||
| switch v.Type { | ||
| case SurveyVarInt: | ||
| switch val := value.(type) { | ||
| case json.Number: | ||
| if _, err := val.Int64(); err == nil { | ||
| return nil | ||
| } | ||
| case string: | ||
| if _, err := strconv.ParseInt(val, 10, 64); err == nil { | ||
| return nil | ||
| } | ||
| } | ||
| return common_errors.NewValidationError("survey variable " + v.Name + " must be an integer") | ||
| case SurveyVarEnum: | ||
| for _, allowed := range v.Values { | ||
| if fmt.Sprintf("%v", value) == allowed.Value { | ||
| return nil | ||
| } | ||
| } | ||
| return common_errors.NewValidationError("survey variable " + v.Name + " has a value which is not allowed") | ||
| } | ||
|
|
||
| return nil | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for any other place that checks SurveyVar.Required
rg -n --type=go '\bRequired\b' db apiRepository: semaphoreui/semaphore
Length of output: 259
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== db/Template.go outline =="
ast-grep outline db/Template.go --view expanded || true
echo
echo "== db/Template.go relevant section =="
sed -n '90,190p' db/Template.go | cat -n
echo
echo "== db/Task.go outline relevant =="
ast-grep outline db/Task.go --view expanded || true
echo
echo "== db/Task.go ValidateSurveyVars and nearby =="
rg -n -C 20 'ValidateSurveyVars|SurveyVar|SurveyVars|Required|json.Unmarshal' db/Task.go db/Template.go
echo
echo "== broader search for Task.ValidateSurveyVars / SurveyVar.Required =="
rg -n 'ValidateSurveyVars|SurveyVar\.Required|Required' .Repository: semaphoreui/semaphore
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Go files containing SurveyVar/required =="
find . -type f -name '*.go' -not -path './bundle/*' -not -path './frontend/*' -not -path './node_modules/*' -print0 2>/dev/null \
| xargs -0 rg -n '\b(SurveyVar|survey variable|SurveyVars)\b|\brequired\b|ValidateValue|ValidateSurveyVars' || true
echo
echo "== db/Template.go lines 100-180 =="
sed -n '100,180p' db/Template.go | cat -n
echo
echo "== db/Task.go focused lines =="
wc -l db/Task.go
rg -n -C 12 'ValidateSurveyVars|SurveyVars|SurveyVar' db/Task.go
echo
echo "== candidate Go files containing ValidateSurveyVars/ValidateValue/Required in tracked source only =="
git ls-files '*.go' | xargs rg -n 'ValidateSurveyVars|ValidateValue|SurveyVar\.Required|Required' db api || trueRepository: semaphoreui/semaphore
Length of output: 19723
Enforce required on missing survey vars.
ValidateValue accepts absent/null values, and Task.ValidateSurveyVars only rejects undeclared or mistyped supplied values; SurveyVar.Required is only a persisted field. Add server-side validation at task creation/validation so omitted required vars fail before execution.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@db/Template.go` around lines 130 - 165, Update Task.ValidateSurveyVars to
validate every declared SurveyVar, not only supplied values, and return a
validation error when a variable with Required set is absent or null. Preserve
ValidateValue’s optional-value behavior for non-required variables, while
ensuring required omissions fail during task creation/validation before
execution.
| this.editedEnvironment = this.filterDeclaredVars(JSON.parse(v.environment || '{}')); | ||
| this.editedSecretEnvironment = JSON.parse(v.secret || '{}'); | ||
| this.hasCommit = v.commit_hash != null; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Filter inherited secret variables too.
The rerun path filters only v.environment; undeclared keys in v.secret remain in editedSecretEnvironment and are serialized by beforeSave. With allow_any_vars_in_task disabled, the backend rejects those keys during ValidateSurveyVars, so rerunning affected tasks fails.
Proposed fix
this.editedEnvironment = this.filterDeclaredVars(JSON.parse(v.environment || '{}'));
- this.editedSecretEnvironment = JSON.parse(v.secret || '{}');
+ this.editedSecretEnvironment = this.filterDeclaredVars(
+ JSON.parse(v.secret || '{}'),
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| this.editedEnvironment = this.filterDeclaredVars(JSON.parse(v.environment || '{}')); | |
| this.editedSecretEnvironment = JSON.parse(v.secret || '{}'); | |
| this.hasCommit = v.commit_hash != null; | |
| this.editedEnvironment = this.filterDeclaredVars(JSON.parse(v.environment || '{}')); | |
| this.editedSecretEnvironment = this.filterDeclaredVars( | |
| JSON.parse(v.secret || '{}'), | |
| ); | |
| this.hasCommit = v.commit_hash != null; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/components/TaskForm.vue` around lines 332 - 334, Update the rerun
initialization near editedEnvironment and editedSecretEnvironment so the parsed
v.secret object also passes through filterDeclaredVars before assignment.
Preserve the existing JSON fallback and ensure editedSecretEnvironment contains
only declared variables, preventing beforeSave from serializing undeclared
secrets.
There was a problem hiding this comment.
Security review
Outcome: No medium/high/critical vulnerabilities found in the added or modified code.
This PR is a security hardening change. It adds backend validation (ValidateSurveyVars) on the primary task-creation API path (POST /project/{id}/tasks) to block undeclared survey variables — including Ansible connection overrides such as ansible_ssh_common_args that can execute code on the Semaphore server.
What was reviewed
- Input → sink tracing:
Task.Environment/Task.Secretare validated againsttemplate.survey_varsbeforeTaskPool.AddTaskruns on the user-facing endpoint. - Controls verified: Undeclared keys are rejected by default; secret survey vars must use
secret; int/enum types are checked;AllowAnyVarsInTaskis opt-in and defaults tofalse. - Bypass paths checked: Integrations, schedules, and autorun child tasks call
TaskPool.AddTaskdirectly without this validation, but those paths only accept environment keys configured by project managers (integration extract mappings / schedule task params), not arbitrary keys from task runners. That is a different trust boundary than the vulnerability this PR fixes. - Other classes: No new injection surfaces, authz bypasses, secret leakage, or dependency changes were introduced.
Residual note (not reported as a finding)
Placing ValidateSurveyVars in the HTTP handler instead of TaskPool.AddTask means future task-creation entry points must remember to call it. Consider centralizing enforcement in TaskPool.AddTask for defense in depth.
Automated security review
Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Suppressed comments (5)
db/Task.go:253
- When AllowAnyVarsInTask is enabled, env keys that aren't declared in the survey should be permitted. Currently the env loop returns an undeclared-variable error unconditionally when GetSurveyVar returns nil, so even after removing the early return the allow-any mode would still reject undeclared env keys.
if v == nil {
return undeclaredSurveyVarError(name)
}
db/Task.go:273
- When AllowAnyVarsInTask is enabled, undeclared keys in the secret payload should be allowed (they stay encrypted/write-only), but declared non-secret vars should still be rejected if sent via Task.Secret. The current combined condition (
v == nil || v.Type != SurveyVarSecret) can't express this distinction.
if v == nil || v.Type != SurveyVarSecret {
return undeclaredSurveyVarError(name)
}
db/Task_test.go:120
- This AllowAnyVarsInTask test currently expects
replicas:"many"(a declared int survey var) to be accepted. If AllowAnyVarsInTask is meant to relax only the undeclared-key restriction (while still validating declared vars and secret handling), change the test data to keep declared vars valid so the test only exercises the allow-any behavior for undeclared keys.
task := Task{
Environment: `{"ansible_ssh_common_args":"-o ProxyCommand=/bin/sh","replicas":"many"}`,
Secret: `{"undeclared":"value"}`,
}
db/Task.go:216
- parseTaskVars currently accepts the JSON literal
nullas valid because decoding into a map yields a nil map without error. This contradicts the intent/error message ("must be a JSON object") and lets clients bypass variable validation by sendingnull.
if err := dec.Decode(&res); err != nil {
return nil, common_errors.NewValidationError("task " + field + " must be a JSON object")
}
db/Task_test.go:88
- Add a regression test for
environment/secretbeing the JSON literalnull. Without this,parseTaskVarscan accidentally acceptnullas a valid object and skip validation.
This issue also appears on line 117 of the same file.
{
name: "environment is not an object",
environment: `["app_version"]`,
errContains: "task environment must be a JSON object",
},
| "suppress_success_alerts, app, git_branch, runner_tag, task_params, "+ | ||
| "allow_override_branch_in_task, allow_parallel_tasks, jwt_params, executor_image)"+ | ||
| "allow_override_branch_in_task, allow_parallel_tasks, allow_any_vars_in_task, "+ | ||
| "jwt_params)"+ | ||
| "allow_override_branch_in_task, allow_parallel_tasks, allow_any_vars_in_task, jwt_params, executor_image)"+ | ||
| "values ("+ |
| if template.AllowAnyVarsInTask { | ||
| return nil | ||
| } |


Summary by CodeRabbit
New Features
Bug Fixes
Documentation