com.cellngine.util.FXMLValidator.java Source code

Java tutorial

Introduction

Here is the source code for com.cellngine.util.FXMLValidator.java

Source

/*
   This file is part of cellngine.
    
   cellngine is free software: you can redistribute it and/or modify
   it under the terms of the GNU Affero General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.
    
   cellngine is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Affero General Public License for more details.
    
   You should have received a copy of the GNU Affero General Public License
   along with cellngine.  If not, see <http://www.gnu.org/licenses/>.
*/
package com.cellngine.util;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.util.Arrays;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Provides methods that check FXML files for security and validity.
 * 
 * @author qwer <hellraz0r.386@googlemail.com>
 */
public class FXMLValidator {
    private static Log LOG = LogFactory.getLog(FXMLValidator.class);

    private static final Charset FXML_CHARSET = Charset.forName("UTF-8");
    private static final AllowedElementsParser ALLOWED_ELEMENTS = getAllowedElements();
    private static final List<String> ALLOWED_IMPORTS = Arrays.asList("java.lang.*", "java.util.*",
            "javafx.geometry.*", "javafx.collections.*", "javafx.scene.control.*", "javafx.scene.effect.*",
            "javafx.scene.image.*", "javafx.scene.input.*", "javafx.scene.layout.*", "javafx.scene.paint.*",
            "javafx.scene.shape.*", "javafx.scene.paint.*", "javafx.scene.text.*", "javafx.scene.web.*");

    private static AllowedElementsParser getAllowedElements() {
        try {
            return new AllowedElementsParser(
                    AllowedElementsParser.class.getResourceAsStream("AllowedElements.txt"));
        } catch (IOException | ParseException e) {
            LOG.error("Could not load FXML allowed elements", e);
        }
        return null;
    }

    /**
     * Provides the same functionality as {@link #validate(InputStream)}, but accepts a string that
     * will be wrapped in a {@link ByteBuffer} for convenience. Since no {@link IOException} should
     * be thrown, all IOExceptions are caught and re-thrown as {@link RuntimeException}s.
     * 
     * @param fxmlAsString
     *            The entire contents of the FXML file.
     * @throws InvalidFXMLException
     *             If the FXML document is invalid or insecure.
     */
    public void validate(final String fxmlAsString) throws InvalidFXMLException {
        if (fxmlAsString == null) {
            throw new NullPointerException();
        }

        final ByteArrayInputStream stream = new ByteArrayInputStream(fxmlAsString.getBytes(FXML_CHARSET));
        try {
            validate(stream);
        } catch (final IOException e) {
            LOG.error("Validating an XML string threw an IOException", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * Reads an FXML document from an {@link InputStream} and parses it. Checks are then performed
     * to ensure that the FXML does not contain anything that might compromise a client trying to
     * display it. This includes unknown components or JavaScript action handlers.
     * 
     * @param in
     *            The {@link InputStream} to read the FXML from.
     * @throws IOException
     *             If an error occurred while retrieving the FXML from the stream.
     * @throws InvalidFXMLException
     *             If the FXML document is invalid or insecure.
     */
    public void validate(final InputStream in) throws IOException, InvalidFXMLException {
        if (in == null) {
            throw new NullPointerException();
        }

        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = null;
        try {
            builder = factory.newDocumentBuilder();
        } catch (final ParserConfigurationException e) {
            LOG.error("Could not initialize XML parser", e);
            return;
        }

        Document document = null;
        try {
            document = builder.parse(in);
        } catch (final SAXException e) {
            throw new InvalidFXMLException("Could not parse XML document", e);
        }

        checkNode(document);
    }

    private void checkNode(final Node node) throws InvalidFXMLException {
        final String nodeName = node.getNodeName();
        final short nodeType = node.getNodeType();

        if (nodeType == Node.ELEMENT_NODE) {
            if (!ALLOWED_ELEMENTS.isElementAllowed(nodeName)) {
                throw new InvalidFXMLException("Element type \"" + nodeName + "\" not allowed");
            }

            final NamedNodeMap nodeAttributes = node.getAttributes();
            for (int i = 0; i < nodeAttributes.getLength(); i++) {
                checkAttributeNode(nodeAttributes.item(i), nodeName);
            }
        } else if (nodeType == Node.TEXT_NODE || nodeType == Node.DOCUMENT_NODE) {
        } else if (nodeType == Node.PROCESSING_INSTRUCTION_NODE && node.getNodeName().equals("import")) {
            if (!ALLOWED_IMPORTS.contains(node.getNodeValue())) {
                throw new InvalidFXMLException("Import \"" + node.getNodeValue() + "\" not allowed.");
            }
        } else if (nodeType != Node.COMMENT_NODE) {
            throw new InvalidFXMLException("Unrecognized node: type: \"" + nodeType + "\", name: \""
                    + node.getNodeName() + "\", value: \"" + node.getNodeValue() + "\"");
        }

        final NodeList nodeChildren = node.getChildNodes();
        for (int i = 0; i < nodeChildren.getLength(); i++) {
            checkNode(nodeChildren.item(i));
        }
    }

    private void checkAttributeNode(final Node node, final String parentName) throws InvalidFXMLException {
        final String nodeName = node.getNodeName();
        final String nodeValue = node.getNodeValue();

        if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
            if (nodeName.equals("fx:factory")) {
                if (!nodeValue.equals("observableArrayList")) {
                    throw new InvalidFXMLException("fx:factory \"" + nodeValue + "\" not allowed");
                }
            } else if (!ALLOWED_ELEMENTS.isAttributeAllowed(parentName, node.getNodeName())) {
                throw new InvalidFXMLException("Attribute \"" + node.getNodeName() + "\" on element type \""
                        + parentName + "\" not allowed");
            }
        } else {
            throw new InvalidFXMLException("Node " + node.getNodeName() + " is not an attribute.");
        }
    }

    /**
     * Thrown when an invalid or insecure XML document is encountered.
     * 
     * @author qwer <hellraz0r.386@googlemail.com>
     */
    public static class InvalidFXMLException extends Exception {
        public InvalidFXMLException(final String message) {
            super(message);
        }

        public InvalidFXMLException(final String message, final Throwable t) {
            super(message, t);
        }

        private static final long serialVersionUID = 722605387466263303L;
    }
}