Conversation
UI / API verification screenshots (PR1)1) Reload existing sensitive workflow — global params maskedSave modal shows:
2) Create / save modal — Sensitive checkbox present3) API mask proof (same workflow) |
Verification screenshots (hosted on fork evidence branch)Reload existing sensitive workflow — value masked + Sensitive checkedSave modal — Sensitive checkbox presentAPI mask proof |
2793cc3 to
b1d4895
Compare
SbloodyS
left a comment
There was a problem hiding this comment.
I found two blocking issues:
- [P1] Sensitive values are still exposed by the definition-version APIs
The new masking is applied to the current workflow/task definition query paths, but the version-list endpoints still return the persisted entities directly:
GET /projects/{projectCode}/task-definition/{code}/versionsreturnsTaskDefinitionLog.taskParams.GET /projects/{projectCode}/workflow-definition/{code}/versionsreturnsWorkflowDefinitionLog.globalParams.
Both SQL projections include these fields, and the service methods put the records into the response without masking them. Therefore, a parameter with sensitive=true can still be read in plaintext by querying its version history, which violates the acceptance criterion that sensitive values must never be returned through external APIs.
Please either mask the returned version entities using the same deep-copy strategy or use dedicated summary DTOs that omit these fields. Regression tests should cover both version endpoints.
- [P1] Updating an old workflow instance can restore the secret from the wrong task version
WorkflowInstanceServiceImpl.mergeSensitiveLocalParams() resolves ****** using taskDefinitionDao.queryByCodes(), which loads the current task definitions. However, the workflow-instance detail shown to the user may have been generated from an older workflow/task version.
If the task's sensitive value changed after that instance was created, editing and saving the old instance will silently replace ****** with the latest task definition's value instead of preserving the value belonging to the displayed instance version.
Please resolve each original value using the matching task definition code and version associated with the instance/submitted task definition, rather than querying only the current definition by code. A regression test should cover updating an old instance after its task secret has changed in a newer version.
|
@SbloodyS Thanks for the review. Both P1s are addressed, with a few extra hardening changes.
|
SbloodyS
left a comment
There was a problem hiding this comment.
Disabling sensitive can silently overwrite the real value with ******
There is still a data-loss path when an existing sensitive parameter is changed to non-sensitive:
- The API returns the parameter as
value = "******", sensitive = true. - The user unchecks Sensitive without editing the value.
- The UI submits
value = "******", sensitive = false. findInvalidSensitivePlaceholderProp()skips the parameter because it is no longer sensitive.mergeSensitiveValuePlaceholder()also skips it for the same reason.- The update therefore persists the literal
******and permanently overwrites the original value.
This affects both workflow global parameters and task localParams. Besides losing the secret, subsequent task executions will receive ****** instead of the configured credential.
Please reject ****** when an existing sensitive parameter is being changed to non-sensitive, requiring the user to enter an explicit replacement value. This validation must be enforced by the backend rather than only by the UI. Restoring the original value while setting sensitive = false would not be safe either, because the next read would expose that value as non-sensitive.
|
@SbloodyS Thank you for the careful review — this is a real data-loss path, and your analysis is correct. Unchecking Sensitive while leaving The backend now rejects this for both workflow global params and task On the UI we chose not to auto-clear Unit tests cover the reject path and the toggle-back keep-original path. As a related hardening, start/backfill also skips a startParam whose value is Thanks again for the detailed write-up. |
SbloodyS
left a comment
There was a problem hiding this comment.
Sensitive global parameters are exposed by create/update responses
The read/query endpoints now mask sensitive parameters, but the write endpoints still return the in-memory WorkflowDefinition containing the merged plaintext globalParams:
WorkflowDefinitionServiceImpl#createWorkflowDefinition()returns the object created from the submitted plaintext parameters.WorkflowDefinitionServiceImpl#updateWorkflowDefinition()returns the object aftermergeGlobalParams()has restored the real value.WorkflowInstanceServiceImpl#updateWorkflowInstance()also returns aWorkflowDefinitioncontaining the merged plaintext value.
The corresponding controllers serialize these objects directly in their Result<WorkflowDefinition> responses. Therefore, creating a sensitive global parameter or updating it with a new value returns the real value through an external API, which violates the acceptance criterion that sensitive parameters must only be returned as ******.
Please return a masked deep copy from these external write paths, for example by applying copyAndMaskWorkflowDefinition() after persistence. Do not mask the object before it has finished being persisted or reuse a masked object for execution.
Please add regression coverage for:
- Creating a workflow with a sensitive global parameter.
- Updating a workflow with a new sensitive value.
- Updating a workflow instance containing a sensitive global parameter.
Each response should contain ******, while the persisted/internal value must remain unchanged and usable for execution.
|
@SbloodyS Thank you — you are right that create/update responses were still serializing the merged plaintext We did not only wrap those three service returns. Query-time masking was too easy to miss on write endpoints (and on any new
Acceptance is unchanged: API/UI still show |
|
@SbloodyS Thank you — the create/update plaintext leak is addressed at the HTTP outbound boundary ( We also added the three regressions you asked for:
Each asserts the HTTP |
SbloodyS
left a comment
There was a problem hiding this comment.
mask(WorkflowInstance) masks globalParams, varPool, and dagData, but leaves commandParam unchanged. The trigger transformer restores ****** to plaintext, WorkflowManualTrigger serializes those values into commandParams, and RunWorkflowCommandHandler persists that JSON on the workflow instance. As a result, GET /workflow-instances/{id} can still expose sensitive start-parameter values through commandParam. Please mask ICommandParam.commandParams in the response copy and add regression coverage for this endpoint.
SbloodyS
left a comment
There was a problem hiding this comment.
Please change your PR's description since the implementation has been changed.
+1, need to mask the commandParams at controller, but the command params doesn't have type, need to find the type from localparam/global param. |
GET /workflow-instances/{id} already masked globalParams/varPool/dagData
but left ICommandParam.commandParams plaintext after start restore.
|
@SbloodyS Thanks for catching these.
Please take another look when you have a chance. |
- Keep plaintext in DB; mask sensitive values on query HTTP - Reject ****** on create; merge it on update; restore startParams from globals Co-authored-by: Cursor <cursoragent@cursor.com>
GET /workflow-instances/{id} already masked globalParams/varPool/dagData
but left ICommandParam.commandParams plaintext after start restore.
8a19fc7 to
bc252d2
Compare
…101/dolphinscheduler into feature-17937-sensitive-property-pr1
SbloodyS
left a comment
There was a problem hiding this comment.
The PR description matches the current controller masking implementation and correctly states that encryption is out of scope. However, its false → true plus ****** behavior conflicts with #18586, which requires that case to be rejected. Please align the issue's acceptance criteria with the intended behavior.
| return taskDefinitionService.queryTaskDefinitionVersions(loginUser, projectCode, code, pageNo, pageSize); | ||
| Result result = taskDefinitionService.queryTaskDefinitionVersions(loginUser, projectCode, code, pageNo, | ||
| pageSize); | ||
| @SuppressWarnings("unchecked") |
There was a problem hiding this comment.
I don't think it's a good way to abuse @SuppressWarnings("unchecked"). We should try to avoid this problem in the official code.
There was a problem hiding this comment.
Thanks for the comment.
@SuppressWarnings("unchecked") is here because the existing service method only returns a raw Result. getData() is Object, so masking the page requires (PageInfo<TaskDefinitionLog>) result.getData(). The same pattern is used in queryWorkflowDefinitionVersions and queryTaskListPaging.
I see two ways to remove it. Which do you prefer?
-
Change the service interface and implementation to
Result<PageInfo<...>>, so the controller can useresult.getData()without a cast. This matches the data the method already returns, but it touches the interface, implementation, and callers/tests. -
Keep the raw
Resultand check withinstanceofin the controller before masking. This avoids a service signature change. On Java 8,instanceofdoes not narrow the type, so a cast is still required after the check, and the controller code is more verbose.
I will update all three paging endpoints the same way once you confirm.
There was a problem hiding this comment.
It is the best way to return a concrete type in the service.
There was a problem hiding this comment.
Updated to option 1: typed the three service methods as Result<PageInfo<...>> (queryTaskDefinitionVersions, queryWorkflowDefinitionVersions, queryTaskListPaging) so the controllers can call result.getData() without @SuppressWarnings("unchecked"). This matches the existing paging API style in this module.
Type paging service returns as Result<PageInfo<T>> and replace generic copyBean masking with concrete mask overloads so controllers no longer need @SuppressWarnings("unchecked").
SbloodyS
left a comment
There was a problem hiding this comment.
GET /workflow-instances/{id} and GET /workflow-instances/{id}/view-variables still return values in plaintext.
Same-named start params copy workflow global attributes and only override value before commandParam persist; mask listCommand/listErrorCommand too.
|
Thanks for the catch. The plaintext came from start/command merge: Map-style Fix (write path, before
After that, Master’s existing whole-object merge keeps the inherited Also masked Verified with a Map-style start ( |
Keep a single mask overload per type hierarchy and branch on instanceof when copying, avoiding CodeQL confusing-overload alerts.


Summary
Implements #18586 (subtask of DSIP-105 / #17937): add
Property.sensitiveand mask sensitive values as******on API/UI, with keep-original merge on write/start.DB and in-process objects stay plaintext. HTTP responses are copy-then-masked in controllers (no
ResponseBodyAdvice).In scope (#18586)
Property.sensitive(defaultfalse; missing JSON field isfalse)SensitivePropertyUtils.mask(...)overloads (WorkflowDefinition/DagData/WorkflowInstance/TaskDefinition/TaskInstance/ view-variableslocalParams). Covers query, version lists, and create/update replies. Copies are masked; Service/DAO objects are not mutated.******means keep the DB original; empty / null is a real empty value******(no previous value). Update rejectstrue → falsewith placeholder-only******(would persist the mask).false → truewith******is keep-original when an existing value existslocalParams; reload echoes******restoreStartParamsreplaces******with the definition global plaintext before the command is sent. Master does not interpret******; the DB never stores the placeholderOut of scope (follow-up subtasks)
PasswordUtilsRelated
Test plan
PropertySensitiveUtilsTest,SensitivePropertyUtilsTest******(globalParamList / localParams / view-variables)Resultis masked without mutating the persisted object******after reloadVerification screenshots
UI — reload workflow: sensitive global param masked as
******UI — save modal: Sensitive checkbox available
API — same workflow masked on query / view-variables
Screenshot branch (fork only, not part of review diff):
det101:pr1-17937-verification-screenshots