Skip to content

Commit

Permalink
prevent that MPR routes modify the hash fragment
Browse files Browse the repository at this point in the history
- Routing based on hash fragment was executing code in
  browser that causes firing client side routing breaking 
  the UI.
- This PR introspects the V7 UIDL in order to replace 
  'window.location.hash' assignation with new API
  'window.history.pushState'.

Fixes #7540
  • Loading branch information
manolo committed Feb 26, 2020
1 parent 0c6a4ce commit e2e57cc
Show file tree
Hide file tree
Showing 2 changed files with 266 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.internal.JavaScriptBootstrapUI;
import com.vaadin.flow.server.HandlerHelper;
import com.vaadin.flow.server.HandlerHelper.RequestType;
import com.vaadin.flow.server.SessionExpiredHandler;
Expand All @@ -37,9 +40,17 @@
import com.vaadin.flow.server.communication.ServerRpcHandler.ResynchronizationRequiredException;
import com.vaadin.flow.shared.JsonConstants;

import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonException;
import elemental.json.JsonObject;
import elemental.json.JsonType;
import elemental.json.impl.JsonUtil;

import static com.vaadin.flow.shared.ApplicationConstants.RPC_INVOCATIONS;
import static com.vaadin.flow.shared.ApplicationConstants.SERVER_SYNC_ID;
import static com.vaadin.flow.shared.JsonConstants.RPC_NAVIGATION_LOCATION;
import static com.vaadin.flow.shared.JsonConstants.UIDL_KEY_EXECUTE;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
Expand All @@ -55,8 +66,20 @@
public class UidlRequestHandler extends SynchronizedRequestHandler
implements SessionExpiredHandler {


private ServerRpcHandler rpcHandler;

public static final Pattern HASH_PATTERN = Pattern.compile("window.location.hash ?= ?'(.*?)'");
public static final Pattern URL_PATTERN = Pattern.compile("^(.*)#(.+)$");
public static final String PUSH_STATE =
"setTimeout(() => history.pushState(null, null, location.pathname + location.search + '#%s'));";

private static final String SYNC_ID = '"' + SERVER_SYNC_ID + '"';
private static final String RPC = RPC_INVOCATIONS;
private static final String LOCATION = RPC_NAVIGATION_LOCATION;
private static final String CHANGES = "changes";
private static final String EXECUTE = UIDL_KEY_EXECUTE;

@Override
protected boolean canHandleRequest(VaadinRequest request) {
return HandlerHelper.isRequestType(request, RequestType.UIDL);
Expand Down Expand Up @@ -116,15 +139,23 @@ private void writeRefresh(VaadinResponse response) throws IOException {
commitJsonResponse(response, json);
}

private static void writeUidl(UI ui, Writer writer, boolean resync)
void writeUidl(UI ui, Writer writer, boolean resync)
throws IOException {
JsonObject uidl = new UidlWriter().createUidl(ui, false, resync);
JsonObject uidl = createUidl(ui, resync);

if (ui instanceof JavaScriptBootstrapUI) {
removeOffendingMprHashFragment(uidl);
}

// some dirt to prevent cross site scripting
String responseString = "for(;;);[" + uidl.toJson() + "]";
writer.write(responseString);
}

JsonObject createUidl(UI ui, boolean resync) {
return new UidlWriter().createUidl(ui, false, resync);
}

private static final Logger getLogger() {
return LoggerFactory.getLogger(UidlRequestHandler.class.getName());
}
Expand Down Expand Up @@ -186,4 +217,105 @@ public static void commitJsonResponse(VaadinResponse response, String json)
// NOTE GateIn requires the buffers to be flushed to work
outputStream.flush();
}

private void removeOffendingMprHashFragment(JsonObject uidl) {
if (!uidl.hasKey(EXECUTE)) {
return;
}

JsonArray exec = uidl.getArray(EXECUTE);
String hash = null;
int idx = -1;
for (int i = 0; i < exec.length(); i++) {
JsonArray arr = exec.get(i);
for (int j = 0; j < arr.length(); j++) {
if (!arr.get(j).getType().equals(JsonType.STRING)) {
continue;
}
String script = arr.getString(j);
if (script.contains("history.pushState")) {
idx = i;
continue;
}
if (!script.startsWith(SYNC_ID)) {
continue;
}
JsonObject json = JsonUtil.parse("{" + script + "}");
hash = removeHashInV7Uidl(json);
if (hash != null) {
script = JsonUtil.stringify(json);
// remove curly brackets
script = script.substring(1, script.length() - 1);
arr.set(j, script);
}
}
}

if (hash != null) {
idx = idx >= 0 ? idx : exec.length();
JsonArray arr = Json.createArray();
arr.set(0, "");
arr.set(1, String.format(PUSH_STATE, hash));
exec.set(idx, arr);
}
}

private String removeHashInV7Uidl(JsonObject json) {
String removed = null;
JsonArray changes = json.getArray(CHANGES);
for (int i = 0; i < changes.length(); i++) {
String hash = removeHashInChange(changes.getArray(i));
if (hash != null) {
removed = hash;
}
}
JsonArray rpcs = json.getArray(RPC);
for (int i = 0; i < rpcs.length(); i++) {
String hash = removeHashInRpc(changes.getArray(i));
if (hash != null) {
removed = hash;
}
}
return removed;
}

private String removeHashInChange(JsonArray change) {
if (change.length() < 3
|| !change.get(2).getType().equals(JsonType.ARRAY)) {
return null;
}
JsonArray value = change.getArray(2);
if (value.length() < 2
|| !value.get(1).getType().equals(JsonType.OBJECT)) {
return null;
}
JsonObject location = value.getObject(1);
if (!location.hasKey(LOCATION)) {
return null;
}
Matcher match = URL_PATTERN.matcher(location.getString(LOCATION));
if (match.find()) {
location.put(LOCATION, match.group(1));
return match.group(2);
}
return null;
}

private String removeHashInRpc(JsonArray rpc) {
if (rpc.length() < 4 || !rpc.get(3).getType().equals(JsonType.ARRAY)) {
return null;
}
JsonArray scripts = rpc.getArray(3);
for (int j = 0; j < scripts.length(); j++) {
String exec = scripts.getString(j);
Matcher match = HASH_PATTERN.matcher(exec);
if (match.find()) {
// replace JS with a noop
scripts.set(j, ";");
return match.group(1);
}
}
return null;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@

import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.Properties;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import com.vaadin.flow.component.internal.JavaScriptBootstrapUI;
import com.vaadin.flow.server.DefaultDeploymentConfiguration;
import com.vaadin.flow.server.HandlerHelper.RequestType;
import com.vaadin.flow.server.VaadinRequest;
Expand All @@ -35,6 +37,17 @@
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.shared.ApplicationConstants;

import elemental.json.JsonObject;
import elemental.json.impl.JsonUtil;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

public class UidlRequestHandlerTest {

private VaadinRequest request;
Expand All @@ -59,9 +72,9 @@ public void writeSessionExpired() throws Exception {
VaadinService service = new VaadinServletService(null,
new DefaultDeploymentConfiguration(getClass(),
new Properties()));
Mockito.when(request.getService()).thenReturn(service);
when(request.getService()).thenReturn(service);

Mockito.when(request
when(request
.getParameter(ApplicationConstants.REQUEST_TYPE_PARAMETER))
.thenReturn(RequestType.UIDL.getIdentifier());

Expand All @@ -80,11 +93,11 @@ public void writeSessionExpired() throws Exception {
@Test
public void writeSessionExpired_whenUINotFound() throws IOException {

VaadinService service = Mockito.mock(VaadinService.class);
VaadinSession session = Mockito.mock(VaadinSession.class);
Mockito.when(session.getService()).thenReturn(service);
VaadinService service = mock(VaadinService.class);
VaadinSession session = mock(VaadinSession.class);
when(session.getService()).thenReturn(service);

Mockito.when(service.findUI(request)).thenReturn(null);
when(service.findUI(request)).thenReturn(null);

boolean result = handler.synchronizedHandleRequest(session, request,
response);
Expand All @@ -99,4 +112,117 @@ public void writeSessionExpired_whenUINotFound() throws IOException {
responseContent);
}

@Test
public void should_not_modifyUidl_when_MPR_nonJavaScriptBootstrapUI() throws Exception {
JavaScriptBootstrapUI ui = null;

UidlRequestHandler handler = spy(new UidlRequestHandler());
StringWriter writer = new StringWriter();
JsonObject uidl = JsonUtil.parse(UIDLString);
uidl.getArray("execute").getArray(2).set(1, V7UIDLString);

doReturn(uidl).when(handler).createUidl(ui, false);

handler.writeUidl(ui, writer, false);

String out = writer.toString();

assertTrue(out.startsWith("for(;;);[{"));
assertTrue(out.endsWith("}]"));

uidl = JsonUtil.parse(out.substring(9, out.length() - 1));

assertTrue(uidl.getArray("execute").getArray(2).getString(1)
.contains("http://localhost:9998/#!away"));

assertEquals(
"setTimeout(() => window.history.pushState(null, '', $0))",
uidl.getArray("execute").getArray(1).getString(1));
}

@Test
public void should_modifyUidl_when_MPR_JavaScriptBootstrapUI() throws Exception {
JavaScriptBootstrapUI ui = mock(JavaScriptBootstrapUI.class);

UidlRequestHandler handler = spy(new UidlRequestHandler());
StringWriter writer = new StringWriter();
JsonObject uidl = JsonUtil.parse(UIDLString);
uidl.getArray("execute").getArray(2).set(1, V7UIDLString);

doReturn(uidl).when(handler).createUidl(ui, false);

handler.writeUidl(ui, writer, false);

String out = writer.toString();
uidl = JsonUtil.parse(out.substring(9, out.length() - 1));

assertFalse(uidl.getArray("execute").getArray(2).getString(1)
.contains("http://localhost:9998/#!away"));
assertTrue(uidl.getArray("execute").getArray(2).getString(1)
.contains("http://localhost:9998/"));

assertEquals(
"setTimeout(() => history.pushState(null, null, location.pathname + location.search + '#!away'));",
uidl.getArray("execute").getArray(1).getString(1));
}

@Test
public void should_not_modify_non_MPR_Uidl() throws Exception {
JavaScriptBootstrapUI ui = mock(JavaScriptBootstrapUI.class);

UidlRequestHandler handler = spy(new UidlRequestHandler());
StringWriter writer = new StringWriter();
JsonObject uidl = JsonUtil.parse(UIDLString);
uidl.getArray("execute").getArray(2).remove(1);

doReturn(uidl).when(handler).createUidl(ui, false);

handler.writeUidl(ui, writer, false);


String expected = uidl.toJson();

String out = writer.toString();
uidl = JsonUtil.parse(out.substring(9, out.length() - 1));

String actual = uidl.toJson();

assertEquals(expected, actual);
}

private String UIDLString =
"{" +
" \"syncId\": 3," +
" \"clientId\": 3," +
" \"changes\": []," +
" \"execute\": [" +
" [\"\", \"document.title = $0\"]," +
" [\"\", \"setTimeout(() => window.history.pushState(null, '', $0))\"]," +
" [[0, 16], \"___PLACE_FOR_V7_UIDL___\", \"$0.firstElementChild.setResponse($1)\"]," +
" [1,null,[0, 16], \"return (function() { this.$server['}p']($0, true, $1)}).apply($2)\"]" +
" ]," +
" \"timings\": []" +
"}";

private String V7UIDLString =
"\"syncId\": 2," +
"\"clientId\": 2," +
"\"changes\": [" +
" [\"change\", {\"pid\": \"0\"}, [\"0\", {\"id\": \"0\", \"location\": \"http://localhost:9998/#!away\"}]]" +
"]," +
"\"state\": {" +
"}," +
"\"types\": {" +
"}," +
"\"hierarchy\": {" +
"}," +
"\"rpc\": [" +
" [" +
" \"11\"," +
" \"com.vaadin.shared.extension.javascriptmanager.ExecuteJavaScriptRpc\"," +
" \"executeJavaScript\", [ \"window.location.hash = '!away';\" ]" +
" ]" +
"]," +
"\"meta\": {}, \"resources\": {},\"typeMappings\": {},\"typeInheritanceMap\": {}, \"timings\": []";

}

0 comments on commit e2e57cc

Please sign in to comment.