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

SecurityError when frontend url !== backend url (even with basename) #3241

Closed
slorber opened this issue Apr 4, 2016 · 27 comments
Closed

SecurityError when frontend url !== backend url (even with basename) #3241

slorber opened this issue Apr 4, 2016 · 27 comments
Labels

Comments

@slorber
Copy link

slorber commented Apr 4, 2016

Version

2.0.0

Steps to reproduce

<base href="http://localhost:8080"/>

Expected Behavior

React-router should work, or at least it should be possible to configure it to make it work

Actual Behavior

Uncaught SecurityError: Failed to execute 'replaceState' on 'History': A history state object with URL 'http://localhost:8080/public/stamples/56fd5227be7e3a4f0770c486/slug' cannot be created in a document with origin 'http://localhost:9000'.

getCurrentLocation @ createBrowserHistory.js?e7cc:60
listen @ createHistory.js?cf40:106
listen @ createDOMHistory.js?4b9b:31
listen @ createBrowserHistory.js?e7cc:144
listen @ useBasename.js?8423:75listen @ 
useQueries.js?d176:108listen @ 
createTransitionManager.js?aa7a:270componentWillMount @ 

Trying with basename:

import { createHistory, useBasename } from 'history'
const history = useRouterHistory(createHistory)({
  basename: 'http://localhost:9000'
});

Same error.

Solution proposal

It seems the error is here:

function getCurrentLocation(historyState) {
    historyState = historyState || window.history.state || {};

    var path = _DOMUtils.getWindowPath();
    var _historyState = historyState;
    var key = _historyState.key;

    var state = undefined;
    if (key) {
      state = _DOMStateStorage.readState(key);
    } else {
      state = null;
      key = history.createKey();

      if (isSupported) window.history.replaceState(_extends({}, historyState, { key: key }), null, path);
    }

    var location = _PathUtils.parsePath(path);

    return history.createLocation(_extends({}, location, { state: state }), undefined, key);
  }

_DOMUtils.getWindowPath(); returns a relative url.

In my experience encoutering a similar problem in the past, when base attribute is set, we should always use absolute urls. You can see it by yourself by running a simple test on a page which have a base attribute:

history.replaceState(undefined,undefined,"/path")
VM3817:2 Uncaught DOMException: Failed to execute 'replaceState' on 'History': A history state object with URL 'http://localhost:8080/path' cannot be created in a document with origin 'http://localhost:9000'.(…)

Here is the original implementation that returns a relative url

function getWindowPath() {
  return window.location.pathname + window.location.search + window.location.hash;
}

By changing it to:

function getWindowPath() {
  return window.location.href;
}

it solves my problem and I don't have security issues anymore. However I have no idea of what else in React-router it could break.

Now I have this warning but it seems to start at least: Warning: A path must be pathname + search + hash only, not a fully qualified URL like "http://localhost:9000/public/stamples/56fd5227be7e3a4f0770c486/slug"

Not really sure it's a History or React-router, feel free to move it needed ;)

@taion
Copy link
Contributor

taion commented Apr 4, 2016

Thanks for your question!

We want to make sure that the GitHub issue tracker remains the best place to track bug reports and feature requests that affect the development of React Router, and per the issue template, we are not handling support requests here.

If you have a question or otherwise need help, please post on Stack Overflow with the #react-router tag at https://stackoverflow.com/questions/ask?tags=react-router, or drop in on the #react-router room on Reactiflux at https://discord.gg/0ZcbPKXt5bYaNQ46.

@slorber
Copy link
Author

slorber commented Apr 4, 2016

@taion I understand the pain of maintaining such a project, and read the things on twitter about the template, and plugin to automatically close issues with message and things like that, but I don't really feel I fall under that case...

I took a lot of time to try to make my issue clear. At first I thought it was an issue with npm and the history module not being deduplicated... Made some more tests... Read the source code of react-router and history and did some tweaks in local...

I would have liked to provide a JsFiddle to reproduce that bug, but as far as I know it's not really possible to use the base attibute in JsFiddle...

Also want to point out that I did found the problem in the source code, and provided a solution which consist of using a fully qualified absolute url in the path of getCurrentLocation function. You can see by yourself that basename is never used inside this function, doesn't it look like a bug to you?

I don't see where you think I'm asking for support, I am here to help actually. If I did not make a PR it's only because I don't know the internals enough yet and would like guidance on how to solve my problem in a proper way (because getWindowPath does work but seems risky to me).


So yes, I feel a bit frustrated that you close this issue so fast without any additional comment :( I feel like future contributors are not really welcome... (because yes, I want to do a PR to solve that problem!)

@taion
Copy link
Contributor

taion commented Apr 4, 2016

There's no bot or plugin here. I read through your issue, as I read through all the issues.

You're using some sort of extremely odd setup here that touches on a number of things that are not explicitly supported. As I said above, please reach out to one of the support forums for help with your setup. This does not look like a bug in React Router or in history.

@slorber
Copy link
Author

slorber commented Apr 4, 2016

@taion thanks for your comment.

You're using some sort of extremely odd setup here

Are you refering to <base href="http://localhost:8080"/> as an extremely odd setup?

I would argue that for me using <base href="http://localhost:8080"/> is not something odd at all. It is standard attribute and indeed it is of great use for anyone that has a backend in a different technology than the frontend. Not everybody is using full-stack javascript. Not supporting this would mean that developers would have to use frontends like Apache/Nginx to be able to expose their backend and frontend on the same domain (probably making them unable to use hot reloading by the way).

that touches on a number of things that are not explicitly supported.

Code directly taken from useBasename:

    // Automatically use the value of <base href> in HTML
    // documents as basename if it's not explicitly given.
    if (basename == null && canUseDOM) {
      const base = document.getElementsByTagName('base')[0]

      if (base)
        basename = extractPath(base.href)
    }

According to the source code I see here, it is clearly expected to support a base attribute. But maybe you only want to support relative basenames?

As I said above, please reach out to one of the support forums for help with your setup. This does not look like a bug in React Router or in history.

@taion clearly I can't post anything to any of these places, because what is the point of asking a question for my setup if React-router does not intend to support that setup? People can only tell me to use something else that will support my usecase...

So the question is: do react-router want to support base path from another domain, or do it only want to support base paths from current domain or relative paths? I wasn't to find anything in documentation. I think it should handle both, and I'm here to propose a solution: always use absolute urls when calling replaceState. I just don't really know exactly which changes to React-Router should be made for that.

If you don't want to support this usecase (or maybe not in this version) just tell me and I'll hack a fork for myself and that's all, but for me I'm really not doing anything fancy. If you want to support it then maybe can you help me make a PR.

@taion
Copy link
Contributor

taion commented Apr 4, 2016

Again, as noted in the project description, for questions and support, please visit our channel on Reactiflux or Stack Overflow. The issue tracker is exclusively for bug reports and feature requests.

@ryanflorence
Copy link
Member

Seems like useBaseName should work but maybe we're using the <basename/> in the html instead of the custom history maybe?

@ryanflorence ryanflorence reopened this Apr 4, 2016
@ryanflorence
Copy link
Member

Not sure if I fully understand what's going on but I think this will work for you?

import { createHistory, useBasename } from 'history'
const history = useRouterHistory(createHistory)({
  basename: '/'
});

slorber added a commit to slorber/history that referenced this issue Apr 5, 2016
@slorber
Copy link
Author

slorber commented Apr 5, 2016

Hi,

@ryanflorence thanks for reopening this.

I've made a reproductible sandbox based on the "active-links" example. The basename attribute works, when you click on a link there is no error. The problem is that on app mounting, React-router tries to replace current history state but does not use the basename in that step.

I also made a pull-request with what seems needed to solve my problem but I'm not sure of the impacts it may have. Note I've done it against History 2.x as it seems 3.x is quite different.

Usecase for using different domains:

  • At Stample.co we have a Scala backend. It is quite heavy and slow to compile and requires developers to install many things and have a good computer (running the backend may take more than 2go ram)
  • We don't want to force frontend developers / integrators to have to run all that in their local computer so we allow them to develop against a remote backend
  • Frontend developers can use remote backend and point them to local assets with an url of type: http://staging.stample.co/?reactAppBaseUrl=http://localhost:8080

@timdorr timdorr added the bug label Apr 6, 2016
slorber added a commit to slorber/history that referenced this issue Apr 8, 2016
@taion
Copy link
Contributor

taion commented Apr 13, 2016

React Router currently doesn't allow linking to other domains. Also, history does something a little bit off with <base href>.

For now, set basename: '' when you create the history. That should make all of your links stay within the same origin. We're looking at how to fix the handling of <base> so as to make this unnecessary on your end.

You can follow along at remix-run/history#94.

@taion taion closed this as completed Apr 13, 2016
@slorber
Copy link
Author

slorber commented Apr 14, 2016

React Router currently doesn't allow linking to other domains. Also, history does something a little bit off with .

@taion you maybe misunderstood the issue because I am NOT trying to link to another domain.

My backend is http://backend.com and it serves an index.html page with my JS app.

I use ReactRouter relative Links like /someResource, and I actually expect ReactRouter to navigate to http://backend.com/someResource , so it's not linking to an outside domain.

The only problem is that my index.html page served by the backend have to define a <base href="http://frontend.com"/>, because all my frontend resources (JS/CSS/Images) are on this other domain.

Because of this base href, the calls history makes to history.replaceState / history.pushState have to be absolute, because otherwise doing `history.pushState(..,..,"/relativeUrl") is actually relative to the base href and is always forbidden.

So I'm using the following

let history = useRouterHistory(createHistory)({
  basename: (window.location.protocol + "//" + window.location.hostname + ":" + window.location.port + "/")
});

With this setup, it actually makes all the history.push("/relativeLocation") be relative to my backend url instead of the frontend url which is forbidden (by making all urls absolutes). It works perfectly (even if maybe basename was not created for that usecase?)

The only problem is on History startup because of this line: window.history.replaceState({ ...historyState, key }, null, path) because here the path is relative.

@taion I wiill stop the discussion on this issue, as finally it's more a History.js problem and no code in React-Router should be changed, and will continue discussion on remix-run/history#267 and remix-run/history#94

@taion
Copy link
Contributor

taion commented Apr 14, 2016

Let's continue to discuss this here, since there's more context available here. This might be a history problem, but it's not one we can fix.

Your current setup is that you have:

  • <base href="http://frontend.com">
  • You are currently on http://backend.com

You have this because you want something like <img src="foo.png"> to resolve to http://frontend.com/foo.png, even though you are on http://backend.com.

However, history is also interpreting http://frontend.com as the "basename". That means that it will treat a link to /foo as a link to http://frontend.com/foo. The security error is ultimately because that's not a legal thing to do.

This is a sort of inconvenient bug in history. We can't fix this without a breaking release. Instead, if you set basename: '' when creating the history, this manually configured basename will override the automatically inferred incorrect one from the <base> tag, and then everything should work.

@slorber
Copy link
Author

slorber commented Apr 14, 2016

@taion the problem is not about the basename being inferred for me. The problem is that on history creation / init, the history tries to do a replaceState to put the history key in the current entry (because it does not have a key at first). It's only during this phase that I have a problem, because at this phase it uses a relative path that could be avoided as it's not needed to

-        window.history.replaceState({ ...historyState, key }, null, path)
+        window.history.replaceState({ ...historyState, key }, null)

Really, just changing that line when you first put the key on current history entry solves my problem :)

If one does not need to change the current url, but only the state, one could simply omit the last parameter and should produce the same result. I think the point of this code is to lazily put the history key inside history state, so removing the last parameter looks safe to me.

A note on using base-href=otherDomain

It is important to note that once a base href has been set to some other domain, then any call to history.setState/replaceState will not work with relative urls because they will not be relative to current domain anymore. I think this is not very nice that browser behave that way for history but it is what it is. So, if using another domain as base, once should ALWAYS use absolute urls when calling history.setState/replaceState

let history = useRouterHistory(createHistory)({
  basename: (window.location.protocol + "//" + window.location.hostname + ":" + window.location.port + "/")
});

This code solves my problem, because when doing HistoryJs.push("/relativeLink"), it gets translated to window.history.pushState(...,...,"http://backend.com/relativeLink") so somehow, the basename permits to make all my relative pushs being "absolutized" against my backend instead of my frontend. Maybe it's not the standard usecase you imagined for basename but it really works fine to solve it.

@taion
Copy link
Contributor

taion commented Apr 14, 2016

History does not use the value of <base href> except via the automatic basename inference in useBasename, on https://github.com/mjackson/history/blob/v2.0.1/modules/useBasename.js#L19.

If you pass in basename: '' when creating the history, you will actually be able to use relative links, which I think is even better than making all of your links absolute.

@slorber
Copy link
Author

slorber commented Apr 14, 2016

@taion basename = '' does not work. Let's not focus on automatic basename inference because I override it anyway (weither it's by the solution I showed you, or with an empty string)

My link has always been relative: <Link to="/my-profile"/>.

Let's consider I never change this link, and see how my basename compare to your basename.

Case 1: basename = ""

let history = useRouterHistory(createHistory)({
  basename: ""
});

If I click on my Link, I get the following error:

Uncaught SecurityError: Failed to execute 'pushState' on 'History': A history state object with URL 'https://frontend.com/my-profile' cannot be created in a document with origin 'https://backend.com'

This is because History.js tries to do window.history.pushState(state,title,"/my-profile"); and once a different domain basename is set, all relative urls here will always fail

Case 2: basename = origin

let history = useRouterHistory(createHistory)({
  basename: "https://backend.com/"
});

// This is same as:
// basename: (window.location.protocol + "//" + window.location.hostname + ":" + window.location.port + "/"),

This time, clicking on my link (which is unchanged, and still relative in my JSX components), it works!

This is because History.js now calls window.history.pushState(state,title,"https://backend.com/my-profile");

Basename is not my problem

As I said, I don't have a problem with basename or useBasename at all since it permits to solve my usecase. My problem is really with these 2 lines that run on createBrowserHistory:

-        window.history.replaceState({ ...historyState, key }, null, path)
+        window.history.replaceState({ ...historyState, key }, null)

Because path is the current relative path, then the code does window.history.replaceState({ ...historyState, key }, null, "/currentRelativePath") which makes it fail because it is resolved as https://frontend.com/currentRelativePath instead of https://backend.com/currentRelativePath

If I were to maintain History.js, I would always call setState/relaceState with absolute urls, because in my opinion and experience building my own routers it always works better

taion pushed a commit to remix-run/history that referenced this issue Apr 14, 2016
When initializing history with initial state, do not provide path to replaceState to let it unchanged

See remix-run/react-router#3241
@taion
Copy link
Contributor

taion commented Apr 14, 2016

I see. Thanks for explaining, and for setting up a fix. I merged your PR on history.

@slorber
Copy link
Author

slorber commented Apr 14, 2016

@taion thanks! :) By chance do you plan a 2.0.2 release soon?

@taion
Copy link
Contributor

taion commented Apr 14, 2016

We'll try. Thanks!

@slorber
Copy link
Author

slorber commented Apr 14, 2016

ok thanks ping me if you release :)

@mjackson
Copy link
Member

I just cut a history 2.0.2 release. Thanks for the fix, @slorber 💃

@slorber
Copy link
Author

slorber commented Apr 15, 2016

awesome thanks :)

@slorber
Copy link
Author

slorber commented Apr 19, 2016

For the record, it's been reported by my team that when a base href of different origin is set, the following fails for Safari: window.history.replaceState({ ...historyState, key }, null) but it works for other browsers.

It can probably be fixed by always providing an absolute url as path. Imho it works on all recent browsers but It's worth testing on older versions I guess. Can you run such tests?

My usecase is for remote dev only as I explained so I can live with it only working in Chrome but if other users are interested in the same kind of stuff working on Safari I'm just reporting the issue

@slorber
Copy link
Author

slorber commented Apr 19, 2016

Note that regression test of @taion seems to have catched some issues with Safari: remix-run/history#274

Note sure it'll fix the problem but it's worth trying to run CI against that: remix-run/history@v2.x...slorber:patch-1

@mjackson
Copy link
Member

@slorber FWIW the 3.x branch shouldn't have this issue at all because we don't issue a replaceState call on the initial page load. You can try it out using npm install history@next

@slorber
Copy link
Author

slorber commented Apr 20, 2016

great :) is History 3.x compatible with currently released react-router or the 2 should be upgraded at the same time?

@taion
Copy link
Contributor

taion commented Apr 20, 2016

@mjackson Don't we still need to get the initial location, and to stamp the location key onto the history state storage when initially rendering the router?

@taion
Copy link
Contributor

taion commented Apr 20, 2016

@slorber Isn't that just window.location.href? Why don't you cherry-pick my patch with the test case, then PR both those changes to see if they pass CI on all browsers?

@mjackson
Copy link
Member

@slorber @taion yes, history 3 isn't compatible w React Router yet. One of the nice things about keeping the projects separate is that we can move the history library forward without moving in lock step w the router. But ya, we'll need some time for the router to catch up once history 3 is stable.

@lock lock bot locked as resolved and limited conversation to collaborators Jan 22, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

5 participants