Skip to content

feat(templates): survey variables valiation on backend - #4108

Open
fiftin wants to merge 2 commits into
developfrom
feat/survey_vars_validation
Open

fiftin wants to merge 2 commits into
developfrom
feat/survey_vars_validation

Conversation

@fiftin

@fiftin fiftin commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Templates can allow tasks to set variables beyond those declared in their survey configuration.
    • Added a template setting and explanatory guidance for enabling this behavior.
    • Task variables now support validation for names, types, enum values, and secret handling.
    • Rerun forms filter inherited variables unsupported by the selected template.
  • Bug Fixes

    • Prevented invalid or undeclared task variables from being submitted.
  • Documentation

    • Improved API documentation for task and template variable behavior.

Copilot AI review requested due to automatic review settings July 30, 2026 05:48
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 02531823-0a15-4c4d-ba9c-fd68d5a86940

📥 Commits

Reviewing files that changed from the base of the PR and between 75ea78c and 73ed7b2.

📒 Files selected for processing (6)
  • api-docs.yml
  • db/Migration.go
  • db/Template.go
  • db/sql/template.go
  • web/src/components/TemplateForm.vue
  • web/src/lang/en.js
🚧 Files skipped from review as they are similar to previous changes (6)
  • web/src/lang/en.js
  • web/src/components/TemplateForm.vue
  • db/Migration.go
  • api-docs.yml
  • db/Template.go
  • db/sql/template.go

📝 Walkthrough

Walkthrough

The change adds template-controlled task survey-variable validation, persists the allow_any_vars_in_task option, validates task creation requests, documents the API behavior, and updates template and task forms.

Changes

Task survey variable handling

Layer / File(s) Summary
Survey variable validation contracts
db/Template.go, db/Task.go, db/Task_test.go
Adds survey-variable lookup and value validation, parses task payloads, enforces declared and secret-variable rules, and tests strict and arbitrary-variable modes.
Template option persistence
db/Migration.go, db/sql/migrations/*, db/sql/template.go, db/sql/migration_2_20_2_test.go
Adds the non-null allow_any_vars_in_task column, migration registration, template create/update persistence, rollback SQL, and persistence tests.
Task creation validation and API schema
api/projects/tasks.go, api-docs.yml
Validates survey variables before task insertion and documents environment, secret, and template option behavior in the API schemas.
Template and task form behavior
web/src/components/TemplateForm.vue, web/src/components/TaskForm.vue, web/src/lang/en.js
Adds the template checkbox and translations, and filters rerun environment variables to declared survey variables unless arbitrary variables are enabled.

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)
Loading

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: backend validation for template survey variables.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/survey_vars_validation

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 as ansible_ssh_common_args that can execute code on the Semaphore server.

What was reviewed

  • Input → sink tracing: Task.Environment / Task.Secret are validated against template.survey_vars before TaskPool.AddTask runs on the user-facing endpoint.
  • Controls verified: Undeclared keys are rejected by default; secret survey vars must use secret; int/enum types are checked; AllowAnyVarsInTask is opt-in and defaults to false.
  • Bypass paths checked: Integrations, schedules, and autorun child tasks call TaskPool.AddTask directly 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

Open in Web View Automation 

Sent by Cursor Automation: Find vulnerabilities

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread api/projects/tasks.go
// 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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex fix it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Moved survey-variable validation into the shared TaskPool.AddTask path, 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.WriteError rather 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)

View task →

Comment thread db/Template.go
// 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"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread db/Template.go
Comment on lines +162 to +164
}

return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread db/Template.go
Comment on lines +155 to +159
case SurveyVarEnum:
for _, allowed := range v.Values {
if fmt.Sprintf("%v", value) == allowed.Value {
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_task template 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 via Task.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.

Comment thread api/projects/tasks.go
Comment on lines +51 to +57
// 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 {
Comment thread db/Task.go
Comment on lines +211 to +218
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
Comment on lines +339 to +345
<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
/>
Comment thread db/Template.go
Comment on lines +142 to +165
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
}
Comment thread db/Task.go
Comment on lines +268 to +274
for name := range secrets {
v := template.GetSurveyVar(name)

if v == nil || v.Type != SurveyVarSecret {
return undeclaredSurveyVarError(name)
}
}
Comment thread db/sql/template.go
Comment on lines 28 to +30
"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)"+

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0bd3589 and 75ea78c.

📒 Files selected for processing (13)
  • api-docs.yml
  • api/projects/tasks.go
  • db/Migration.go
  • db/Task.go
  • db/Task_test.go
  • db/Template.go
  • db/sql/migration_2_20_2_test.go
  • db/sql/migrations/v2.20.2.err.sql
  • db/sql/migrations/v2.20.2.sql
  • db/sql/template.go
  • web/src/components/TaskForm.vue
  • web/src/components/TemplateForm.vue
  • web/src/lang/en.js

Comment thread api/projects/tasks.go
Comment on lines +51 to +61
// 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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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' || true

Repository: 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' || true

Repository: 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
done

Repository: 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.

Comment thread db/Task.go
Comment on lines +201 to +219
// 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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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.

Comment thread db/Template.go
Comment on lines +130 to +165
// 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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 api

Repository: 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 || true

Repository: 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.

Comment on lines +332 to 334
this.editedEnvironment = this.filterDeclaredVars(JSON.parse(v.environment || '{}'));
this.editedSecretEnvironment = JSON.parse(v.secret || '{}');
this.hasCommit = v.commit_hash != null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Copilot AI review requested due to automatic review settings July 31, 2026 09:48

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Secret are validated against template.survey_vars before TaskPool.AddTask runs on the user-facing endpoint.
  • Controls verified: Undeclared keys are rejected by default; secret survey vars must use secret; int/enum types are checked; AllowAnyVarsInTask is opt-in and defaults to false.
  • Bypass paths checked: Integrations, schedules, and autorun child tasks call TaskPool.AddTask directly 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

Open in Web View Automation 

Sent by Cursor Automation: Find vulnerabilities

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 null as 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 sending null.
	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/secret being the JSON literal null. Without this, parseTaskVars can accidentally accept null as 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",
		},

Comment thread db/sql/template.go
Comment on lines 28 to 32
"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 ("+
Comment thread db/Task.go
Comment on lines +234 to +236
if template.AllowAnyVarsInTask {
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants