Description
Writing a variable set that contains a sensitive variable with an empty Value and no Id silently destroys the stored secret. Nothing errors, and the loss is invisible on read because the server never echoes a sensitive value back.
Variable.Value is a string, so a caller who rebuilds a variable set rather than round-tripping the objects returned by GetAll naturally produces exactly that payload.
Steps To Reproduce
- Create a project variable with
IsSensitive: true and a known value.
- Write the variable set back with that variable's
Value empty and its Id omitted.
- Resolve the variable in a runbook or deployment — it is now empty.
Expected Behavior
Either the secret is preserved, or the call fails. Silently replacing a stored secret with an empty string is the one outcome a caller cannot detect.
Any Logs and/or Other Supporting Information
Verified against a live 2026.x server. A runbook printed ${#VALUE} for the sensitive variable after each write, so masking could not hide the result. Starting value was 18 characters:
Id sent |
Value sent |
Resulting length |
| yes |
"" |
18 — preserved |
| yes |
null |
18 — preserved |
| yes |
"newvalue123" |
11 — updated |
| no |
"" |
0 — wiped |
| no |
null |
0 — wiped |
| no |
"newvalue123" |
11 — updated |
The Id is the sole determinant. null and "" behave identically in both directions.
This matters for #261, which proposes changing Value to *string on the premise that "when updating a sensitive variable, the value needs to be null". On this server that has no effect — rows 4 and 5 above are identical — so that change would be breaking for every caller of Variable.Value without fixing the data loss. Recording the measurements here so the fix targets the Id, not the null.
Environment and/or Versions
- Octopus Server Version: 2026.x (local)
- Go Version: go1.22
- go-octopusdeploy:
main @ 1b925bc
Description
Writing a variable set that contains a sensitive variable with an empty
Valueand noIdsilently destroys the stored secret. Nothing errors, and the loss is invisible on read because the server never echoes a sensitive value back.Variable.Valueis astring, so a caller who rebuilds a variable set rather than round-tripping the objects returned byGetAllnaturally produces exactly that payload.Steps To Reproduce
IsSensitive: trueand a known value.Valueempty and itsIdomitted.Expected Behavior
Either the secret is preserved, or the call fails. Silently replacing a stored secret with an empty string is the one outcome a caller cannot detect.
Any Logs and/or Other Supporting Information
Verified against a live 2026.x server. A runbook printed
${#VALUE}for the sensitive variable after each write, so masking could not hide the result. Starting value was 18 characters:IdsentValuesent""null"newvalue123"""null"newvalue123"The
Idis the sole determinant.nulland""behave identically in both directions.This matters for #261, which proposes changing
Valueto*stringon the premise that "when updating a sensitive variable, the value needs to be null". On this server that has no effect — rows 4 and 5 above are identical — so that change would be breaking for every caller ofVariable.Valuewithout fixing the data loss. Recording the measurements here so the fix targets theId, not the null.Environment and/or Versions
main@ 1b925bc