Skip to content

fix: sanitize password in frontend render#8857

Open
dmadisetti wants to merge 7 commits intomainfrom
dm/mo-5366
Open

fix: sanitize password in frontend render#8857
dmadisetti wants to merge 7 commits intomainfrom
dm/mo-5366

Conversation

@dmadisetti
Copy link
Copy Markdown
Collaborator

📝 Summary

The PR prevents the backend from passing passwords to the frontend. There's a bit of additional logic to make the text box editable in a meaningful way.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment Mar 26, 2026 10:29pm

Request Review

@dmadisetti dmadisetti added the bug Something isn't working label Mar 24, 2026
@dmadisetti dmadisetti marked this pull request as ready for review March 25, 2026 17:58
Copilot AI review requested due to automatic review settings March 25, 2026 17:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 aims to prevent password values from being rendered into the frontend DOM for ui.text(kind="password"), while still keeping the Python-side value intact and making the password field editable without immediately overwriting the backend value.

Changes:

  • Backend: mask password initial_value in rendered HTML and add a password-has-value flag.
  • Frontend: add masked-placeholder behavior for password inputs and remount logic keyed off random-id.
  • Tests: add a unit test for password masking behavior and a new smoke test app for manual verification.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
marimo/_plugins/ui/_impl/input.py Sends an empty initial_value to the frontend for password inputs with a non-empty default and adds a password-has-value arg.
frontend/src/plugins/impl/TextInputPlugin.tsx Uses passwordHasValue to show a masked placeholder and suppress value propagation until the user types; forces remount on cell rerun.
tests/_plugins/ui/_impl/test_input.py Adds a unit test ensuring password defaults aren’t embedded in the HTML output.
marimo/_smoke_tests/inputs/password_masking.py Adds a smoke test app covering multiple debounce modes for password inputs.

Comment on lines +756 to 760
is_password_with_value = kind == "password" and bool(value)
super().__init__(
component_name=text._name,
initial_value=value,
initial_value="" if is_password_with_value else value,
label=label,
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Masking passwords by passing initial_value="" changes the UIElement's frontend initial value and therefore breaks behaviors that depend on the element’s initial frontend value (notably cloning via _clone() used by .form(), .batch(), ui.array, etc.). A cloned password input will initialize with an empty value (losing the provided default), and a form submit without edits would submit an empty string and overwrite the backend’s stored password. Consider implementing masking at HTML serialization / frontend protocol level (e.g., separate “frontend_initial_value” from the backend value, or a sentinel meaning “unchanged”) so clones/forms preserve the backend value without embedding it in the DOM.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this is valid @dmadisetti . If I do

form = mo.ui.text(value="Hello!", kind="password").form()
form
form.value

The initial value doesn't appear

Comment on lines +101 to 103
const [valueOnBlur, setValueOnBlur] = useState(props.value);
const valueToValidate = valueOnBlur == null ? value : valueOnBlur;
const isValid = validate(props.kind, valueToValidate);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

valueOnBlur is initialized from props.value but never updated when props.debounce === true (the OnBlurredInput path doesn’t call setValueOnBlur). As a result, isValid can remain stuck on the initial value and the invalid border state won’t reflect user-entered email/url values after blur/Enter. Updating valueOnBlur when props.value changes (or deriving valueToValidate directly from the displayed/internal value for the OnBlurredInput case) would fix this.

Copilot uses AI. Check for mistakes.
Comment on lines +299 to +303
# HTML should NOT contain the real password
assert "secret123" not in t.text
# data-initial-value should be empty string
assert "data-initial-value" in t.text

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The test comment says data-initial-value should be an empty string, but the assertion only checks that the attribute exists. This would still pass if the initial value were accidentally populated (as long as it doesn’t contain the exact substring secret123). Consider asserting the actual initial value is empty (e.g., via parsing the data-initial-value attribute) to make the test match the intended guarantee.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +93
const value = masked ? "" : props.value;
const placeholder = masked ? MASK_PLACEHOLDER : props.placeholder;
const setValue: Setter<T> = masked
? (v) => {
if (!hasTyped.current) {
return;
}
setMasked(false);
props.setValue(v);
}
: props.setValue;
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The masking flow sets value to an empty string while masked is true and suppresses setValue until typing begins. With the backend now holding a non-empty password that the frontend doesn’t know, any higher-level components that read the UI registry/initial value (e.g., forms) can treat the value as empty and overwrite the stored password on submit unless the protocol supports an “unchanged password” sentinel. It may be worth extending the contract so “password has value but not revealed” round-trips safely through form submit / buffered states without clearing the backend value.

Copilot uses AI. Check for mistakes.
Comment on lines +756 to 760
is_password_with_value = kind == "password" and bool(value)
super().__init__(
component_name=text._name,
initial_value=value,
initial_value="" if is_password_with_value else value,
label=label,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this is valid @dmadisetti . If I do

form = mo.ui.text(value="Hello!", kind="password").form()
form
form.value

The initial value doesn't appear

@dmadisetti
Copy link
Copy Markdown
Collaborator Author

Nice catch @Light2Dark !

Screen.Recording.2026-03-26.at.3.25.12.PM.mov

Copy link
Copy Markdown
Collaborator

@Light2Dark Light2Dark left a comment

Choose a reason for hiding this comment

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

I tested a different approach, if you do want to check it out
#8892

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants