Closed koto closed 5 years ago
After reading the spec...
I think my original proposal (checking only the function body) is simply not sane.
Likewise, I think checking parameters separately is effectively useless, since it'd be super easy to 'hide' things by splitting functionality between different bits.
My current thinking is to pass the entire array of arguments to the callback, which could work.
However, after having re-read the spec on this... maybe all of this is too complicated. The spec specifies how to assemble all of the args into a single source string, which means just not special casing this at all would seem to work just fine:
I.e., do the trusted type check at or around step 18 of CreateDynamicFunction, with possibly some custom logic that if it's a single argument which is a 'safe' type, to then use that instead.
So, have a single parameter type check early in the algorithm, and if it passes, the stringified function is interpreted as bodyText
, with the assembly steps skipped?
They can just assemble the source string themselves and pass that through a policy.
That would require changing the Function constructor to support single-parameter function body with arguments calls (but since it's typed, that should work).
There is some JS spec machinery here to build, so deferring to Mike on how easy it is to add to the spec. If we have @@evalable
, perhaps it's an easy task in the end.
We can call the default policy with the fully assembled string. (I had previously assumed that the source string assembly was an implementation detail one wouldn't expose to the developer, since every browser might to this differently, but in reality this is all fully spec-ed.)
How does this work when body is a TrustedScript and the arguments are strings?
Yeah, I think there are two options:
Neither of these need to affect the order of stringification in CreateDynamicFunction, but the second might affect whether something like Function('a a', '')
results in a TypeError or a SyntaxError which might raise web-compat issues.
I don't think this affects generality though. Just how easy it is to express complicated list arguments. Obviously, any call that passes arguments like
new Function(x, policy.createScript(body))
can be rewritten to one that does not
TrustedTypes.isScript(body) && new Function(
policy.createScript(`return function (${ x }) { ${ body } }`)
which means that different tolerances for argument safety can be specified in user code.
@otherdaniel Should we go through the dynamic semantics of FormalArgumentList and figure out what's a good tradeoff?
@koto @mikesamuel Honestly, I feel like I'm out of my depth, here, since every time I re-read the spec I discover some reason why my previous argument doesn't quite hold...
@koto: "have a single parameter type check early in the algorithm" Yeah. I'm no longer sure that's best, though.
@mikesamuel: "Should we go through the dynamic semantics [...]" Yes, we should. I'd prefer to have a list of options, though, so we can present that to V8 team for fact-checking. Or maybe just get Toon in there.
Function('a', 'b', 'return a+b;') => Function("return function(a,b) { return a+b; }");
That is kind of awkward. Also, if that was our choice, it's interesting what the 'default' policy should receive as an argument.
The Function constructor could then (via the embedder) check for that type and - if found - unwrap the argument list from the trusted type and proceed as normal. This might be the cleanest to specify, but arguably more work to implement.
E.g., in addition to createHTML, createURL, createScript, createScriptURL, there's a createFunction(...args), which returns a TrustedFunction. TrustedFunction isn't string-ifiable, and the Function constructor is the only thing reading it.
Re #2 do the below fit the two argument case?
new Function('a, b, c', 'return a+b+c')
new Function(myPolicy.createScript('a, b, c'), myPolicy.createScript('return a+b+c'))
I like the idea of using IsSimpleParameterList since that'll be more palatable for the committee.
Does the following flow fit #2?
The host callout does:
E.g., in addition to createHTML, createURL, createScript, createScriptURL, there's a createFunction(...args), which returns a TrustedFunction. TrustedFunction isn't string-ifiable, and the Function constructor is the only thing reading it.
Would a single suffice work for all the Function constructors: Function
, AsyncFunction
, etc. ?
@koto As discussed, here's a user-library class that can stand in for all arguments:
class C {
*[Symbol.iterator]() {
for (let a of [...this.args, this.body]) { yield a }
}
constructor(...args) {
this.args = args.slice(0, args.length - 1); this.body = args[args.length - 1] || '';
}
toString() {
return `function (${ this.args.join(', ') }\n) { ${ this.body } }`;
}
}
//undefined
const c = new C('a', 'b, c', 'return [a, b, c]')
//undefined
Array.from(c)
// (3) ["a", "b, c", "return [a, b, c]"]
new Function(...c)
// ƒ anonymous(a,b, c
// ) {
// return [a, b, c]
// }
That class could serve as a basis for the new type (TrustedFunctionConstructorArguments
:) ). It would wrap over multiple strings essentially, last of which is a function body.
With that iterable type instance in place, one would call a Function constructor, spreading the arguments.
The default policy is also accounted for - at the right place, its createFunctionConstructorArguments
is being called with the original string (or mixed string and typed) arguments, returning the iterator (or an array of TrustedScripts
, whichever is easier to spec.
"Does the following flow fit #2?" Yes, that's the idea. I'm not sure if specifying works that way. I guess we'd need an algorithm that does something like:
And in CreateDynamicFunction, do someting like:
2 args: P=ThatNewAlgorithm(string-ify procedure), body=ThatNewAlgorithm(args[N-1]) Note that for that last case, P will always 'loose' the trusted value.
On second thought, I do think a new 'trusted function' type that wraps all the arguments is a more understandable way. In that case, there could be one check for the trusted type early on, and then the entire procedure could continue undisturbed.
Is this a fair statement of requirements:
new Function(...simpleParameterStrings, trustedBody)
to worknew Function(...simpleParameterStrings, stringBody)
to work out of the box with a default policy callout for stringBody.new Function(...complexParameters, trustedBody)
new Function(...mixOfStringAndTrustedParameters, anyBody)
worked but not required.and similarly for {Async,}{Generator,}Function
?
Is the idea that
Host...
stringifies argument[i] it MUST stringify all of arguments[0:i+1] in left-to-right orderHost...
receives one argument which is to be parsed as a ScriptBody, it MUST return an argument list of length 1. (To not require eval
to do complicated error recovery after the host callout).I think that algorithm augmentation would work. Thanks Daniel and Mike for noticing and hashing that out.
On Thu, May 30, 2019, 10:54 Mike Samuel notifications@github.com wrote:
Is the idea that
- CreateDynamicFunction passes arguments to the host callout before any stringification, concatenation, or FormalArgumentList validity checks.
- The host callout returns an argument list
- The host callout has 2 new normative notes:
- If Host... stringifies argument[i] it MUST stringify all of arguments[0:i+1] in left-to-right order
- If Host... receives one argument which is to be parsed as a ScriptBody, it MUST return an argument list of length 1. (To not require eval to do complicated error recovery after the host callout).
- After the host callout, CreateDynamicFunction stringifies the returned arguments and proceeds.
- Browser's implementation of the host callout can use the default policy to convert args to a TrustedFunctionBitsAndBobs if arguments is not [...simpleParameters, TrustedScript] or [ TrustedFunctionBitsAndBobs].
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/WICG/trusted-types/issues/174?email_source=notifications&email_token=AAA7JK6UKSFBO6YDETE76NDPX6B2NA5CNFSM4HPIQPUKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODWRUWWY#issuecomment-497240923, or mute the thread https://github.com/notifications/unsubscribe-auth/AAA7JK2UAOGKGCGMTTHMAEDPX6B2NANCNFSM4HPIQPUA .
@koto, IIUC, Toon noticed this problem. I missed it entirely.
New proposal text at https://mikesamuel.github.io/proposal-hostensurecancompilestrings-passthru/ and I adjusted the explainer.
First n-1 parameters to Function constructor form the argument lists, but still have the capability of executing code due to a weird way how a function is constructed (string concatenation, essentially):
https://www.ecma-international.org/ecma-262/9.0/index.html#sec-createdynamicfunction
It would be best if all of the function parameters were passed to a single (default) policy
createScript
function, instead of separate invocations for each parameter, but that means thecreateScript
function signature changes (argument list and the meaning of the return value).cc @otherdaniel @mikesamuel