With more and more application logic moving to the client, and with YUI becoming more popular on the server, it's increasingly important to design APIs that handle user input safely. Currently, YUI modules that store user input in attributes must do one of two things: either escape user strings before setting an attribute, or escape them manually before using them.
Escaping automatically before storing the value is safest, but also inconvenient if you sometimes need the unescaped value, since you must then store two versions (probably in two different attributes). This can lead to API clutter and confusion. Escaping manually before use avoids API clutter but increases the likelihood of mistakes, and also clutters up the codebase in general. It significantly increases the chances that another developer who is unaware of the need to escape the value will inadvertently introduce a security vulnerability.
Attribute should provide a consistent, pluggable API for retrieving a filtered string value. Internally, string attributes would be stored as raw strings, and would be filtered on demand using the specified filter when the attribute is retrieved. This avoids the need for module developers to write custom getter functions or store both filtered and unfiltered values, and allows for flexible and safe usage in a variety of scenarios.
Filters should be pluggable via a static API on Y.Attribute
. This will allow the escape
module to provide a set of default filters, and custom modules can provide their own filters to meet custom needs.
Setting a value continues to work the same as it does today:
klass.set('username', '<b>joe</b>'); // stores "<b>joe</b>" as the raw attr value
Getting a value also works the same:
klass.get('username'); // => "<b>joe</b>"
To get a filtered version of a value, specify the name of the desired filter as the second argument to get()
:
klass.get('username', 'html'); // => "<b>joe</b>"
klass.get('username', 'url'); // => "%3Cb%3Ejoe%3C%2Fb%3E"
Filters are registered statically on Y.Attribute
. Once registered, a filter is available for use on any class instance that uses Attribute, even if it was instantiated before the filter was registered.
To register a filter:
// Registers a new "html" filter unless one already exists.
Y.Attribute.addFilter('html', Y.Escape.html);
// Arbitrary filter.
Y.Attribute.addFilter('disemvowel', function (value) {
return value.replace(/[aeiou]+/g, '');
});
Internally, Attribute should store the filter function in a static object hash, with the name as the key.
If addFilter()
is called with the name of a filter that already exists, it should log an error and refuse to overwrite the existing filter.
To remove a previously added filter:
// Removes the "html" filter if it exists.
Y.Attribute.removeFilter('html');
When removeFilter()
is called with the name of a filter that doesn't exist, it should simply do nothing.
A new attribute config property named filter
would allow module developers to specify a default filter to be used for an attribute. For example, I could define an attribute that should always be filtered as HTML by default:
// ...
ATTRS: {
username: {
filter: 'html'
}
}
// ...
This would cause get('username')
to run the "html" filter. I could still specify another filter if desired, or get('username', 'raw')
to get the raw, unfiltered value.
To improve performance, Attribute could cache filtered values internally, clearing the cached value whenever an attribute's raw value is updated. There may be dragons here.
Internally, get()
or its underyling implementation should take the following steps:
-
Let
attrName
be the value of the first argument toget()
. -
Let
filterName
be the value of the second argument toget()
. -
If
attrName
refers to a nonexistent attribute, returnundefined
. -
Let
rawValue
be the raw value of the attribute or sub-attribute named byattrName
, after passing through the attribute's getter function if one is set. -
If
filterName
isundefined
ornull
, then-
If a default filter has been configured for the attribute, then
-
Let
filterName
be the name of the attribute's default filter. -
If
filterName
refers to a nonexistant filter, returnundefined
. -
Execute the default filter function, passing
rawValue
as the only argument. -
Return the filter function's return value.
-
-
Otherwise, return
rawValue
.
-
-
Otherwise:
-
If
filterName
equals "raw", returnrawValue
. -
Otherwise, if
filterName
refers to a nonexistent filter, returnundefined
. -
Execute the filter function, passing
rawValue
as the only argument. -
Return the filter function's return value.
-
@jinsley: Returning the
rawValue
there when it's expected to be filtered could have even worse consequences, which is why we returnundefined
if the specified filter isn't found. It might also make sense to log an error in this case.