/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package com.xpn.xwiki.internal.store.hibernate.query; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.Function; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.select.FromItem; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectBody; import net.sf.jsqlparser.statement.select.SelectExpressionItem; import net.sf.jsqlparser.statement.select.SelectItem; /** * Provide various SQL related utilities. * * @version $Id: 9114a0f445ab26cf47b1d2081f91a7be515df0ab $ * @since 7.2M2 */ public final class HqlQueryUtils { private static final String DOCUMENT_FIELD_FULLNAME = "fullName"; private static final String DOCUMENT_FIELD_NAME = "name"; private static final String DOCUMENT_FIELD_SPACE = "space"; private static final String DOCUMENT_FIELD_LANGUAGE = "language"; private static final String DOCUMENT_FIELD_DEFAULTLANGUAGE = "defaultLanguage"; private static final String DOCUMENT_FIELD_TRANSLATION = "translation"; private static final String DOCUMENT_FIELD_HIDDEN = "hidden"; private static final String SPACE_FIELD_REFERENCE = "reference"; private static final String SPACE_FIELD_NAME = DOCUMENT_FIELD_NAME; private static final String SPACE_FIELD_PARENT = "parent"; private static final String SPACE_FIELD_HIDDEN = DOCUMENT_FIELD_HIDDEN; private static final String FROM_REPLACEMENT = "$1"; private static final Pattern FROM_DOC = Pattern.compile("com\\.xpn\\.xwiki\\.doc\\.([^ ]+)"); private static final Pattern FROM_OBJECT = Pattern.compile("com\\.xpn\\.xwiki\\.objects\\.([^ ]+)"); private static final Pattern FROM_RCS = Pattern.compile("com\\.xpn\\.xwiki\\.doc\\.rcs\\.([^ ]+)"); private static final Pattern FROM_VERSION = Pattern.compile("com\\.xpn\\.xwiki\\.store\\.migration\\.([^ ]+)"); private static final Map<String, Set<String>> ALLOWED_FIELDS; static { ALLOWED_FIELDS = new HashMap<>(); Set<String> allowedDocFields = new HashSet<>(); ALLOWED_FIELDS.put("XWikiDocument", allowedDocFields); allowedDocFields.add(DOCUMENT_FIELD_FULLNAME); allowedDocFields.add(DOCUMENT_FIELD_NAME); allowedDocFields.add(DOCUMENT_FIELD_SPACE); allowedDocFields.add(DOCUMENT_FIELD_LANGUAGE); allowedDocFields.add(DOCUMENT_FIELD_DEFAULTLANGUAGE); allowedDocFields.add(DOCUMENT_FIELD_TRANSLATION); allowedDocFields.add(DOCUMENT_FIELD_HIDDEN); Set<String> allowedSpaceFields = new HashSet<>(); ALLOWED_FIELDS.put("XWikiSpace", allowedSpaceFields); allowedSpaceFields.add(SPACE_FIELD_REFERENCE); allowedSpaceFields.add(SPACE_FIELD_NAME); allowedSpaceFields.add(SPACE_FIELD_PARENT); allowedSpaceFields.add(SPACE_FIELD_HIDDEN); } private static final Logger LOGGER = LoggerFactory.getLogger(HqlQueryUtils.class); private HqlQueryUtils() { } /** * @param statement the statement to evaluate * @return true if the statement is complete, false otherwise */ public static boolean isShortFormStatement(String statement) { return StringUtils.startsWithAny(statement.trim().toLowerCase(), ",", "from", "where", "order"); } /** * @param statementString the SQL statement to check * @return true if the passed SQL statement is allowed */ public static boolean isSafe(String statementString) { try { // TODO: should probably use a more specific Hql parser // FIXME: Workaround https://github.com/JSQLParser/JSqlParser/issues/163 (Support class syntax in HQL query) String cleanedStatement = statementString; cleanedStatement = FROM_DOC.matcher(cleanedStatement).replaceAll(FROM_REPLACEMENT); cleanedStatement = FROM_OBJECT.matcher(cleanedStatement).replaceAll(FROM_REPLACEMENT); cleanedStatement = FROM_RCS.matcher(cleanedStatement).replaceAll(FROM_REPLACEMENT); cleanedStatement = FROM_VERSION.matcher(cleanedStatement).replaceAll(FROM_REPLACEMENT); Statement statement = CCJSqlParserUtil.parse(cleanedStatement); if (statement instanceof Select) { Select select = (Select) statement; SelectBody selectBody = select.getSelectBody(); if (selectBody instanceof PlainSelect) { PlainSelect plainSelect = (PlainSelect) selectBody; Map<String, String> tables = getTables(plainSelect); for (SelectItem selectItem : plainSelect.getSelectItems()) { if (!isSelectItemAllowed(selectItem, tables)) { return false; } } return true; } } } catch (JSQLParserException e) { // We can't parse it so lets say it's not safe LOGGER.warn("Failed to parse request [{}] ([{}]). Considering it not safe.", statementString, ExceptionUtils.getRootCauseMessage(e)); } return false; } private static Map<String, String> getTables(PlainSelect plainSelect) { Map<String, String> tables = new HashMap<>(); // Add from item addFromItem(plainSelect.getFromItem(), tables); // Add joins List<Join> joins = plainSelect.getJoins(); if (joins != null) { for (Join join : joins) { addFromItem(join.getRightItem(), tables); } } return tables; } private static void addFromItem(FromItem item, Map<String, String> tables) { if (item instanceof Table) { String tableName = ((Table) item).getName(); tables.put(item.getAlias() != null ? item.getAlias().getName() : tableName, tableName); } } /** * @param selectItem the {@link SelectItem} to check * @return true if the passed {@link SelectItem} is allowed */ private static boolean isSelectItemAllowed(SelectItem selectItem, Map<String, String> tables) { if (selectItem instanceof SelectExpressionItem) { SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem; return isSelectExpressionAllowed(selectExpressionItem.getExpression(), tables); } // TODO: we could support more select items return false; } private static boolean isSelectExpressionAllowed(Expression expression, Map<String, String> tables) { if (expression instanceof Column) { Column column = (Column) expression; if (isColumnAllowed(column, tables)) { return true; } } else if (expression instanceof Function) { Function function = (Function) expression; if (function.isAllColumns()) { // Validate that allowed table is passed to the method // TODO: add support for more that "count" maybe return function.getName().equals("count") && tables.size() == 1 && isTableAllowed(tables.values().iterator().next()); } else { // Validate that allowed columns are used as parameters for (Expression parameter : function.getParameters().getExpressions()) { if (!isSelectExpressionAllowed(parameter, tables)) { return false; } } return true; } } return false; } /** * @param column the {@link Column} to check * @return true if the passed {@link Column} is allowed */ private static boolean isColumnAllowed(Column column, Map<String, String> tables) { Set<String> fields = ALLOWED_FIELDS.get(getTableName(column.getTable(), tables)); return fields != null && fields.contains(column.getColumnName()); } /** * @param tableName the name of the table * @return true if the table has at least one allowed field */ private static boolean isTableAllowed(String tableName) { return ALLOWED_FIELDS.containsKey(tableName); } private static String getTableName(Table table, Map<String, String> tables) { String tableName = tables.values().iterator().next(); if (table != null && StringUtils.isNotEmpty(table.getFullyQualifiedName())) { tableName = tables.get(table.getFullyQualifiedName()); } return tableName; } }