js-loaders
js-loaders copied to clipboard
Same Origin Policy, <script src=>, and load()
Per @samth, in the browser, cross-origin <script src=>
loads do go through the fetch
hook, but they quietly skip the translate
and link
hooks. (This is to avoid revealing cross-origin text to user code.)
Once a cross-origin script is fetched, when we process its imports, we do call normalize
, resolve
and fetch
as normal.
That isn't implemented. It isn't clear how it could be implemented!
- We do not know before calling
fetch
whether a URL is cross-origin or not. Redirects and CORS headers make it impossible to know a priori. -
System.fetch
has to fail if you give it a cross-origin URL. - We think
loader.load()
should have feature parity with<script src=>
, which means being able to load scripts across origins.
Taken together, this means certain parts of the Loader need to be privileged—they need to be able to parse code from a different origin, which ordinary user scripts aren't even permitted to see. I think we have to design a back channel or extension point between Loader and the browser.
I suggested this:
- When
System.fetch
fails due to the same-origin policy, but<script src=>
would succeed,System.fetch
calls thereject
callback passing aDOMException
with the response data stored in an internal property. - The Loader's
reject
callback checks for that internal property on the exception object. Finding it, we let the load proceed (rather than failing asreject
normally does) but we skip thetranslate
andlink
hooks for the current load.
In the code, this would mean browser-loader.js
would have to import, at least, a secret function setCrossOriginFetchPayload(exc, payload)
from impl.js
.
I think it's OK for this to be ugly, within reason.
I think this is a little over-complicated. I like the user-visible behavior, but I don't think we should spec the weird internal unobservable reject/exception/private state interaction. Just state that the resulting code goes directly to the compiler, without passing through any more hooks.
It's all observable.
What I proposed is observably different from, say, stack inspection. It's transparent to lambda:
let originalFetch = System.fetch;
System.fetch = (...args) => originalFetch(...args);
Stack inspection would see a non-privileged caller. It's transparent to using setTimeout to sever the stack:
System.fetch = (...args) => setTimeout(() => originalFetch(...args), 0);
Stack inspection would see only non-privileged code.
Another possible implementation would be for System.fetch to examine the fulfill hook and only pass secret data to the real Loader fulfill hook. But that wouldn't be transparent to lambda:
System.fetch = (addr, fulfill, reject) =>
originalFetch(addr, (s, a) => fulfill(s, a), exc => reject(exc));
What I proposed is transparent to this kind of callback-wrapping. (And I expect this is something users will actually do.)
I am totally open to alternatives, but this weirdness is smack in the middle of a user hook; a great deal about the interaction will inevitably be observable.
I agree that stack inspection is the wrong solution. I was thinking of your second solution, but if you think that consequence is not ok, then I'm open to your original solution.
One thing to note is that if some opaque response is added (that's what Navigation Controllers do, btw), then we'll change from calling reject to calling fulfill.
OK, we're on the same page then.
I wonder if what Navigation Controllers plan on doing here is really OK. Even an opaque response exposes success/failure of requests against cross-domain URLs.
@dherman says that @wycats may have thoughts on this.
The plan of record is to do what the navcontrollers spec does. Same problem, same solution. I'll add a primitive to the implementation and code that shows what the control flow should be if the fetch hook produces an opaque object representing a cross-origin response.
To clarify, if there were CORS headers the translate
and link
hooks would still work fine though?
Yes.
Polymorphism for src
gets us this. the "plan of record" from 3 months ago is basically right.
This is still open because, as I said 3 months ago: "I'll add a primitive to the implementation and code that shows what the control flow should be..." The decision is made; the implementation needs to catch up.