Securing the server-side when hacking the client-side

Hi!

This question is for Vaadin developers.
What is the best way to implement control over the substitution of data in the template model on the client side (or the state of data components and unresolved calls to server handlers)?
Example:
There are valid values (some ids): [1,2,3]
.
The part of them was setted as a combobox items: [1,2]
.
On the client-side we “patch” the item1.value: 1 → 3.
Thus when we select item1, the sever-side model get unresolved value (id=3!).
Also, we can “patch” the disabled button and make it enabled and press it.

That tools exists (or planned) in Vaadin for simplify this checks?

So far, I see this solution only:

  1. The own Model use (a checks implementation, always correct) + TemplateModel (for easy get/set ui values only).
  2. The @EventHandler onCahnge methods usage for check and change values on Model.

But… I would like something more simple and concise, without cumbersome or duplicate code.

p.s.
Is way to prevent fake incoming TemplateModel changes (mSync) from client-side?
Example:

java:
	public interface UIModel extends TemplateModel {
		void setId(String sid);
		String getId();
	}

html:
	<vaadin-text-field value="[[id]
]"/>  // only for read

In this case, if I generate a mSync fake event with a new id value on the client … Will the server receive it and change the TemplateModel?

There isn’t any built-in feature specifically for this case, and I don’t think there’s anything planned either. It might indeed be good to introduce some way of defining validator callbacks for various parts of the template model. In addition to the workarounds you’re suggesting, it might also work to define a model property converter (@Encode) that rejects invalid values.

The situation is better for the case with read-only properties. By default, the server only allows updates from the client if the template contains a two-way binding for that property. This can be configured specifically for each model property using the @AllowClientUpdates annotation.

Ok. But using @Encode for this case will generate too much extra code.
In addition, there is the problem of restoring the value on the client side. For there is no way to manual REsend the value of the model property from the server to the client if the value on the server has not changed.
Considering this fact, there is only one way - to allow the property on the server side to accept the wrong value, and then process this change on the server side and change it to a valid value. And for this you need to have a separate (safe and valid) data model, in addition to the template model.

p.s.
Are the quotation buttons broken?

You can make the correction immediately using an element-level property change listener.

On the other hand, these kinds of invalid values from the original question is either caused by a programming error or by someone trying to tamper with the application’s logic. In both of those cases, a hard error might be a good option. Silently correcting the value has the risk of making assumptions that don’t reflect the user’s actual intention.

As an exercise for the mind, this is a simple way of detecting or filtering incoming changes in model properties (RPC mSync preprocessing). I took one of the Vaadin testcases.

public interface HasModelSyncHandler {
    JsonObject ModelSyncHandler(StateNode node, String name, JsonObject invocationJson);
}

public class ProbeView extends PolymerTemplate<ProbeView.UIModel> implements HasModelSyncHandler {

	public interface UIModel extends TemplateModel {
        String getIdFirm();
        void setIdFirm(String idFirm);
    }
	
    @Override
    public JsonObject ModelSyncHandler(StateNode node, String name, JsonObject invocationJson) {
        logger.info("FIRE SYNC HANDLER FOR: " + name);
        if (name.equals("idFirm")) {
            String value = invocationJson.getString("value");
            if ("1".equals(value)) {
                logger.info("Prevent value = " + value);
                getModel().setIdFirm(null);
                //node.markAsDirty(); // How to cause sending the old value to the client?
                return null;
            }
        }
        return invocationJson;
    }
}

public class AppServlet extends VaadinServlet {

	private static class MapSyncServletService extends VaadinServletService {
        public MapSyncServletService(VaadinServlet servlet, DeploymentConfiguration deploymentConfiguration) {
            super(servlet, deploymentConfiguration);
        }

        @Override
        protected List<RequestHandler> createRequestHandlers() throws ServiceException {
            List<RequestHandler> handlers = super.createRequestHandlers();
            ArrayList<RequestHandler> newHandlers = new ArrayList<>(handlers.size());
            for (int i = 0; i < handlers.size(); i++) {
                RequestHandler handler = handlers.get(i);
                if (handler instanceof UidlRequestHandler) {
                    newHandlers.add(new MapSyncUidlRequestHandler());
                } else {
                    newHandlers.add(handler);
                }
            }
            return newHandlers;
        }
    }

    private static class MapSyncUidlRequestHandler extends UidlRequestHandler {
        @Override
        protected ServerRpcHandler createRpcHandler() {
            return new MapSyncServerRpcHandler(); 
        }
    }

    private static class MapSyncServerRpcHandler extends ServerRpcHandler {
        @Override
        protected Map<String, RpcInvocationHandler> getInvocationHandlers() {
            Map<String, RpcInvocationHandler> map = super.getInvocationHandlers();

            map = new HashMap<>(map);
            map.put(JsonConstants.RPC_TYPE_MAP_SYNC, new TestMapSyncRpcHandler());
            return map;
        }
    }

    private static class TestMapSyncRpcHandler extends MapSyncRpcHandler {
        @Override
        protected Optional<Runnable> handleNode(StateNode node, JsonObject invocationJson) {
            String property = invocationJson.getString(JsonConstants.RPC_PROPERTY);
            ComponentMapping feature = node.getFeature(ComponentMapping.class);
            Optional<Component> optcomp = feature.getComponent();
            if (optcomp.isPresent()) {
                Component comp = optcomp.get();
                if (HasModelSyncHandler.class.isAssignableFrom(comp.getClass())) {
                    invocationJson = ((HasModelSyncHandler)comp).ModelSyncHandler(node, property, invocationJson);
                    if (invocationJson == null) return Optional.empty(); // Is it correct?
                }
            }
            return super.handleNode(node, invocationJson);
        }
    }

    @Override
    protected VaadinServletService createServletService(DeploymentConfiguration deploymentConfiguration) throws ServiceException {
        MapSyncServletService service = new MapSyncServletService(this, deploymentConfiguration);
        service.init();
        return service;
    }
}

Leif Åstrand:
You can make the correction immediately using an element-level property change listener.

I am using this method now.

public class ProbeView extends PolymerTemplate<ProbeView.UIModel> {

	public interface UIModel extends TemplateModel {
        String getIdFirm();
        void setIdFirm(String idFirm);
    }
	
	private final ProbeViewModel model; // safe model

    public ProbeView() {
        model = new ProbeViewModel(getModel());

        Element element = getElement();
        element.addPropertyChangeListener("idFirm", this::onIdFirmChange);
    }

    private void onIdFirmChange(PropertyChangeEvent event) {
        String value = (String)event.getValue();
        if (!CommonTools.isEqualValues(model.getIdFirm(), value)) {
            if ("103".equals(value)) {
                getModel().setIdFirm(model.getIdFirm()); // restore old value
            } else {
                model.selectFirm(value); // change model value 
            }
        }
    }
}

Leif Åstrand:
On the other hand, these kinds of invalid values from the original question is either caused by a programming error or by someone trying to tamper with the application’s logic. In both of those cases, a hard error might be a good option. Silently correcting the value has the risk of making assumptions that don’t reflect the user’s actual intention.

I agree, such situations require separate processing. But it also happens that you just need to return the old valid value.