This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch camel-2.25.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit 2a7cef7733a0995f61c7245f0451f1a554f01bd1 Author: Colm O hEigeartaigh <cohei...@apache.org> AuthorDate: Tue Feb 18 12:31:58 2020 +0000 CAMEL-14532 - Fix issues with camel-snakeyaml --- .../component/snakeyaml/SnakeYAMLDataFormat.java | 66 +- .../custom/CustomClassLoaderConstructor.java | 92 +++ .../component/snakeyaml/custom/CustomComposer.java | 276 ++++++++ .../snakeyaml/custom/CustomConstructor.java | 85 +++ .../snakeyaml/custom/CustomSafeConstructor.java | 85 +++ .../camel/component/snakeyaml/custom/Yaml.java | 711 +++++++++++++++++++++ .../component/snakeyaml/SnakeYAMLDoSTest.java | 156 +++++ .../src/test/resources/data-dos.yaml | 11 + .../camel-snakeyaml/src/test/resources/data.yaml | 2 + 9 files changed, 1467 insertions(+), 17 deletions(-) diff --git a/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormat.java b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormat.java index e012b7a..2218bc8 100644 --- a/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormat.java +++ b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormat.java @@ -33,17 +33,18 @@ import java.util.function.Function; import org.apache.camel.CamelContext; import org.apache.camel.Exchange; +import org.apache.camel.component.snakeyaml.custom.CustomClassLoaderConstructor; +import org.apache.camel.component.snakeyaml.custom.CustomConstructor; +import org.apache.camel.component.snakeyaml.custom.CustomSafeConstructor; +import org.apache.camel.component.snakeyaml.custom.Yaml; import org.apache.camel.spi.DataFormat; import org.apache.camel.spi.DataFormatName; import org.apache.camel.support.ServiceSupport; import org.apache.camel.util.IOHelper; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.TypeDescription; -import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.BaseConstructor; import org.yaml.snakeyaml.constructor.Constructor; -import org.yaml.snakeyaml.constructor.CustomClassLoaderConstructor; -import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.nodes.Tag; import org.yaml.snakeyaml.representer.Representer; import org.yaml.snakeyaml.resolver.Resolver; @@ -66,6 +67,9 @@ public final class SnakeYAMLDataFormat extends ServiceSupport implements DataFor private boolean prettyFlow; private boolean allowAnyType; private List<TypeFilter> typeFilters; + private int maxAliasesForCollections = 50; + private boolean allowRecursiveKeys; + public SnakeYAMLDataFormat() { this(null); @@ -131,7 +135,8 @@ public final class SnakeYAMLDataFormat extends ServiceSupport implements DataFor this.constructor.apply(context), this.representer.apply(context), this.dumperOptions.apply(context), - this.resolver.apply(context) + this.resolver.apply(context), + maxAliasesForCollections ); yamlCache.set(new WeakReference<>(yaml)); @@ -333,29 +338,51 @@ public final class SnakeYAMLDataFormat extends ServiceSupport implements DataFor this.allowAnyType = allowAnyType; } + public int getMaxAliasesForCollections() { + return maxAliasesForCollections; + } + + /** + * Set the maximum amount of aliases allowed for collections. + */ + public void setMaxAliasesForCollections(int maxAliasesForCollections) { + this.maxAliasesForCollections = maxAliasesForCollections; + } + + public boolean isAllowRecursiveKeys() { + return allowRecursiveKeys; + } + + /** + * Set whether recursive keys are allowed. + */ + public void setAllowRecursiveKeys(boolean allowRecursiveKeys) { + this.allowRecursiveKeys = allowRecursiveKeys; + } + // *************************** // Defaults // *************************** private BaseConstructor defaultConstructor(CamelContext context) { - ClassLoader yamlClassLoader = this.classLoader; Collection<TypeFilter> yamlTypeFilters = this.typeFilters; - - if (yamlClassLoader == null && useApplicationContextClassLoader) { - yamlClassLoader = context.getApplicationContextClassLoader(); - } - if (allowAnyType) { yamlTypeFilters = Collections.singletonList(TypeFilters.allowAll()); } BaseConstructor yamlConstructor; if (yamlTypeFilters != null) { + ClassLoader yamlClassLoader = this.classLoader; + if (yamlClassLoader == null && useApplicationContextClassLoader) { + yamlClassLoader = context.getApplicationContextClassLoader(); + } + yamlConstructor = yamlClassLoader != null - ? typeFilterConstructor(yamlClassLoader, yamlTypeFilters) - : typeFilterConstructor(yamlTypeFilters); + ? typeFilterConstructor(yamlClassLoader, yamlTypeFilters, allowRecursiveKeys) + : typeFilterConstructor(yamlTypeFilters, allowRecursiveKeys); } else { - yamlConstructor = new SafeConstructor(); + yamlConstructor = new CustomSafeConstructor(); + ((CustomSafeConstructor)yamlConstructor).setAllowRecursiveKeys(allowRecursiveKeys); } if (typeDescriptions != null && yamlConstructor instanceof Constructor) { @@ -394,8 +421,8 @@ public final class SnakeYAMLDataFormat extends ServiceSupport implements DataFor // Constructors // *************************** - private static Constructor typeFilterConstructor(final Collection<TypeFilter> typeFilters) { - return new Constructor() { + private static Constructor typeFilterConstructor(final Collection<TypeFilter> typeFilters, boolean allowRecursiveKeys) { + CustomConstructor constructor = new CustomConstructor() { @Override protected Class<?> getClassForName(String name) throws ClassNotFoundException { if (typeFilters.stream().noneMatch(f -> f.test(name))) { @@ -405,10 +432,13 @@ public final class SnakeYAMLDataFormat extends ServiceSupport implements DataFor return super.getClassForName(name); } }; + constructor.setAllowRecursiveKeys(allowRecursiveKeys); + return constructor; } - private static Constructor typeFilterConstructor(final ClassLoader classLoader, final Collection<TypeFilter> typeFilters) { - return new CustomClassLoaderConstructor(classLoader) { + private static Constructor typeFilterConstructor(final ClassLoader classLoader, final Collection<TypeFilter> typeFilters, + boolean allowRecursiveKeys) { + CustomClassLoaderConstructor constructor = new CustomClassLoaderConstructor(classLoader) { @Override protected Class<?> getClassForName(String name) throws ClassNotFoundException { if (typeFilters.stream().noneMatch(f -> f.test(name))) { @@ -418,5 +448,7 @@ public final class SnakeYAMLDataFormat extends ServiceSupport implements DataFor return super.getClassForName(name); } }; + constructor.setAllowRecursiveKeys(allowRecursiveKeys); + return constructor; } } diff --git a/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/CustomClassLoaderConstructor.java b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/CustomClassLoaderConstructor.java new file mode 100644 index 0000000..7efabe1 --- /dev/null +++ b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/CustomClassLoaderConstructor.java @@ -0,0 +1,92 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.camel.component.snakeyaml.custom; + +import java.util.List; +import java.util.Map; + +import org.yaml.snakeyaml.constructor.ConstructorException; +import org.yaml.snakeyaml.error.Mark; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; + +/** + * A CustomClassLoaderConstructor which picks up the options to disallow recursive keys + * + * NOTE - If this PR gets applied then we can remove it: + * https://bitbucket.org/asomov/snakeyaml/pull-requests/55/allow-configuration-for-preventing-billion/diff + */ +public class CustomClassLoaderConstructor extends org.yaml.snakeyaml.constructor.CustomClassLoaderConstructor { + + private boolean allowRecursiveKeys; + + public CustomClassLoaderConstructor(ClassLoader cLoader) { + super(cLoader); + } + + public CustomClassLoaderConstructor(Class<? extends Object> theRoot, ClassLoader theLoader) { + super(theRoot, theLoader); + } + + @Override + protected void constructMapping2ndStep(MappingNode node, Map<Object, Object> mapping) { + List<NodeTuple> nodeValue = node.getValue(); + for (NodeTuple tuple : nodeValue) { + Node keyNode = tuple.getKeyNode(); + Node valueNode = tuple.getValueNode(); + Object key = constructObject(keyNode); + if (key != null) { + try { + key.hashCode();// check circular dependencies + } catch (Exception e) { + throw new CustomConstructorException("while constructing a mapping", + node.getStartMark(), "found unacceptable key " + key, + tuple.getKeyNode().getStartMark(), e); + } + } + Object value = constructObject(valueNode); + if (keyNode.isTwoStepsConstruction()) { + if (allowRecursiveKeys) { + postponeMapFilling(mapping, key, value); + } else { + throw new YAMLException("Recursive key for mapping is detected but it is not configured to be allowed."); + } + } else { + mapping.put(key, value); + } + } + } + + public boolean isAllowRecursiveKeys() { + return allowRecursiveKeys; + } + + public void setAllowRecursiveKeys(boolean allowRecursiveKeys) { + this.allowRecursiveKeys = allowRecursiveKeys; + } + + private static class CustomConstructorException extends ConstructorException { + public CustomConstructorException(String context, Mark contextMark, String problem, + Mark problemMark, Throwable cause) { + super(context, contextMark, problem, problemMark, cause); + } + } +} \ No newline at end of file diff --git a/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/CustomComposer.java b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/CustomComposer.java new file mode 100644 index 0000000..74dfcc4 --- /dev/null +++ b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/CustomComposer.java @@ -0,0 +1,276 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.camel.component.snakeyaml.custom; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.yaml.snakeyaml.composer.Composer; +import org.yaml.snakeyaml.composer.ComposerException; +import org.yaml.snakeyaml.error.Mark; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.events.AliasEvent; +import org.yaml.snakeyaml.events.Event; +import org.yaml.snakeyaml.events.MappingStartEvent; +import org.yaml.snakeyaml.events.NodeEvent; +import org.yaml.snakeyaml.events.ScalarEvent; +import org.yaml.snakeyaml.events.SequenceStartEvent; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeId; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.SequenceNode; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.parser.Parser; +import org.yaml.snakeyaml.resolver.Resolver; + +/** + * Creates a node graph from parser events. + * <p> + * Corresponds to the 'Compose' step as described in chapter 3.1 of the + * <a href="http://yaml.org/spec/1.1/">YAML Specification</a>. + * </p> + * + * NOTE - This is a slight port of the Composer class in SnakeYaml to specify the maxAliasesForCollections + * and also use CustomComposer + CustomConstructor. If this PR gets applied then we can remove it: + * https://bitbucket.org/asomov/snakeyaml/pull-requests/55/allow-configuration-for-preventing-billion/diff + */ +public class CustomComposer extends Composer { + private final Resolver resolver; + private final Map<String, Node> anchors; + private final Set<Node> recursiveNodes; + private int nonScalarAliasesCount = 0; + private final int maxAliasesForCollections; + + public CustomComposer(Parser parser, Resolver resolver) { + this(parser, resolver, 50); + } + + public CustomComposer(Parser parser, Resolver resolver, int maxAliasesForCollections) { + super(parser, resolver); + this.resolver = resolver; + this.anchors = new HashMap<String, Node>(); + this.recursiveNodes = new HashSet<Node>(); + this.maxAliasesForCollections = maxAliasesForCollections; + } + + /** + * Checks if further documents are available. + * + * @return <code>true</code> if there is at least one more document. + */ + public boolean checkNode() { + // Drop the STREAM-START event. + if (parser.checkEvent(Event.ID.StreamStart)) { + parser.getEvent(); + } + // If there are more documents available? + return !parser.checkEvent(Event.ID.StreamEnd); + } + + /** + * Reads and composes the next document. + * + * @return The root node of the document or <code>null</code> if no more + * documents are available. + */ + public Node getNode() { + // Drop the DOCUMENT-START event. + parser.getEvent(); + // Compose the root node. + Node node = composeNode(null); + // Drop the DOCUMENT-END event. + parser.getEvent(); + //clean up resources + this.anchors.clear(); + this.recursiveNodes.clear(); + return node; + } + + /** + * Reads a document from a source that contains only one document. + * <p> + * If the stream contains more than one document an exception is thrown. + * </p> + * + * @return The root node of the document or <code>null</code> if no document + * is available. + */ + public Node getSingleNode() { + // Drop the STREAM-START event. + parser.getEvent(); + // Compose a document if the stream is not empty. + Node document = null; + if (!parser.checkEvent(Event.ID.StreamEnd)) { + document = getNode(); + } + // Ensure that the stream contains no more documents. + if (!parser.checkEvent(Event.ID.StreamEnd)) { + Event event = parser.getEvent(); + Mark contextMark = document != null ? document.getStartMark(): null; + throw new CustomComposerException("expected a single document in the stream", + contextMark, "but found another document", event.getStartMark()); + } + // Drop the STREAM-END event. + parser.getEvent(); + return document; + } + + private Node composeNode(Node parent) { + if (parent != null) recursiveNodes.add(parent); + final Node node; + if (parser.checkEvent(Event.ID.Alias)) { + AliasEvent event = (AliasEvent) parser.getEvent(); + String anchor = event.getAnchor(); + if (!anchors.containsKey(anchor)) { + throw new CustomComposerException(null, null, "found undefined alias " + anchor, + event.getStartMark()); + } + node = anchors.get(anchor); + if (!(node instanceof ScalarNode)) { + this.nonScalarAliasesCount++; + if (this.nonScalarAliasesCount > maxAliasesForCollections) { + throw new YAMLException("Number of aliases for non-scalar nodes exceeds the specified max=" + maxAliasesForCollections); + } + } + if (recursiveNodes.remove(node)) { + node.setTwoStepsConstruction(true); + } + } else { + NodeEvent event = (NodeEvent) parser.peekEvent(); + String anchor = event.getAnchor(); + // the check for duplicate anchors has been removed (issue 174) + if (parser.checkEvent(Event.ID.Scalar)) { + node = composeScalarNode(anchor); + } else if (parser.checkEvent(Event.ID.SequenceStart)) { + node = composeSequenceNode(anchor); + } else { + node = composeMappingNode(anchor); + } + } + recursiveNodes.remove(parent); + return node; + } + + protected Node composeScalarNode(String anchor) { + ScalarEvent ev = (ScalarEvent) parser.getEvent(); + String tag = ev.getTag(); + boolean resolved = false; + Tag nodeTag; + if (tag == null || tag.equals("!")) { + nodeTag = resolver.resolve(NodeId.scalar, ev.getValue(), + ev.getImplicit().canOmitTagInPlainScalar()); + resolved = true; + } else { + nodeTag = new Tag(tag); + } + Node node = new ScalarNode(nodeTag, resolved, ev.getValue(), ev.getStartMark(), + ev.getEndMark(), ev.getScalarStyle()); + if (anchor != null) { + node.setAnchor(anchor); + anchors.put(anchor, node); + } + return node; + } + + protected Node composeSequenceNode(String anchor) { + SequenceStartEvent startEvent = (SequenceStartEvent) parser.getEvent(); + String tag = startEvent.getTag(); + Tag nodeTag; + boolean resolved = false; + if (tag == null || tag.equals("!")) { + nodeTag = resolver.resolve(NodeId.sequence, null, startEvent.getImplicit()); + resolved = true; + } else { + nodeTag = new Tag(tag); + } + final ArrayList<Node> children = new ArrayList<Node>(); + SequenceNode node = new SequenceNode(nodeTag, resolved, children, startEvent.getStartMark(), + null, startEvent.getFlowStyle()); + if (anchor != null) { + node.setAnchor(anchor); + anchors.put(anchor, node); + } + while (!parser.checkEvent(Event.ID.SequenceEnd)) { + children.add(composeNode(node)); + } + Event endEvent = parser.getEvent(); + node.setEndMark(endEvent.getEndMark()); + return node; + } + + protected Node composeMappingNode(String anchor) { + MappingStartEvent startEvent = (MappingStartEvent) parser.getEvent(); + String tag = startEvent.getTag(); + Tag nodeTag; + boolean resolved = false; + if (tag == null || tag.equals("!")) { + nodeTag = resolver.resolve(NodeId.mapping, null, startEvent.getImplicit()); + resolved = true; + } else { + nodeTag = new Tag(tag); + } + + final List<NodeTuple> children = new ArrayList<NodeTuple>(); + MappingNode node = new MappingNode(nodeTag, resolved, children, startEvent.getStartMark(), + null, startEvent.getFlowStyle()); + if (anchor != null) { + node.setAnchor(anchor); + anchors.put(anchor, node); + } + while (!parser.checkEvent(Event.ID.MappingEnd)) { + composeMappingChildren(children, node); + } + Event endEvent = parser.getEvent(); + node.setEndMark(endEvent.getEndMark()); + return node; + } + + protected void composeMappingChildren(List<NodeTuple> children, MappingNode node) { + Node itemKey = composeKeyNode(node); + if (itemKey.getTag().equals(Tag.MERGE)) { + node.setMerged(true); + } + Node itemValue = composeValueNode(node); + children.add(new NodeTuple(itemKey, itemValue)); + } + + protected Node composeKeyNode(MappingNode node) { + return composeNode(node); + } + + protected Node composeValueNode(MappingNode node) { + return composeNode(node); + } + + private static class CustomComposerException extends ComposerException { + + protected CustomComposerException(String context, Mark contextMark, String problem, + Mark problemMark) { + super(context, contextMark, problem, problemMark); + // TODO Auto-generated constructor stub + } + + } +} diff --git a/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/CustomConstructor.java b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/CustomConstructor.java new file mode 100644 index 0000000..5257742 --- /dev/null +++ b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/CustomConstructor.java @@ -0,0 +1,85 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.camel.component.snakeyaml.custom; + +import java.util.List; +import java.util.Map; + +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.constructor.ConstructorException; +import org.yaml.snakeyaml.error.Mark; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; + +/** + * A CustomConstructor which picks up the options to disallow recursive keys + * + * NOTE - If this PR gets applied then we can remove it: + * https://bitbucket.org/asomov/snakeyaml/pull-requests/55/allow-configuration-for-preventing-billion/diff + */ +public class CustomConstructor extends Constructor { + + private boolean allowRecursiveKeys; + + @Override + protected void constructMapping2ndStep(MappingNode node, Map<Object, Object> mapping) { + List<NodeTuple> nodeValue = node.getValue(); + for (NodeTuple tuple : nodeValue) { + Node keyNode = tuple.getKeyNode(); + Node valueNode = tuple.getValueNode(); + Object key = constructObject(keyNode); + if (key != null) { + try { + key.hashCode();// check circular dependencies + } catch (Exception e) { + throw new CustomConstructorException("while constructing a mapping", + node.getStartMark(), "found unacceptable key " + key, + tuple.getKeyNode().getStartMark(), e); + } + } + Object value = constructObject(valueNode); + if (keyNode.isTwoStepsConstruction()) { + if (allowRecursiveKeys) { + postponeMapFilling(mapping, key, value); + } else { + throw new YAMLException("Recursive key for mapping is detected but it is not configured to be allowed."); + } + } else { + mapping.put(key, value); + } + } + } + + public boolean isAllowRecursiveKeys() { + return allowRecursiveKeys; + } + + public void setAllowRecursiveKeys(boolean allowRecursiveKeys) { + this.allowRecursiveKeys = allowRecursiveKeys; + } + + private static class CustomConstructorException extends ConstructorException { + public CustomConstructorException(String context, Mark contextMark, String problem, + Mark problemMark, Throwable cause) { + super(context, contextMark, problem, problemMark, cause); + } + } +} \ No newline at end of file diff --git a/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/CustomSafeConstructor.java b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/CustomSafeConstructor.java new file mode 100644 index 0000000..d93d6c5 --- /dev/null +++ b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/CustomSafeConstructor.java @@ -0,0 +1,85 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.camel.component.snakeyaml.custom; + +import java.util.List; +import java.util.Map; + +import org.yaml.snakeyaml.constructor.ConstructorException; +import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.error.Mark; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; + +/** + * A CustomSafeConstructor which picks up the options to disallow recursive keys + * + * NOTE - If this PR gets applied then we can remove it: + * https://bitbucket.org/asomov/snakeyaml/pull-requests/55/allow-configuration-for-preventing-billion/diff + */ +public class CustomSafeConstructor extends SafeConstructor { + + private boolean allowRecursiveKeys; + + @Override + protected void constructMapping2ndStep(MappingNode node, Map<Object, Object> mapping) { + List<NodeTuple> nodeValue = node.getValue(); + for (NodeTuple tuple : nodeValue) { + Node keyNode = tuple.getKeyNode(); + Node valueNode = tuple.getValueNode(); + Object key = constructObject(keyNode); + if (key != null) { + try { + key.hashCode();// check circular dependencies + } catch (Exception e) { + throw new CustomConstructorException("while constructing a mapping", + node.getStartMark(), "found unacceptable key " + key, + tuple.getKeyNode().getStartMark(), e); + } + } + Object value = constructObject(valueNode); + if (keyNode.isTwoStepsConstruction()) { + if (allowRecursiveKeys) { + postponeMapFilling(mapping, key, value); + } else { + throw new YAMLException("Recursive key for mapping is detected but it is not configured to be allowed."); + } + } else { + mapping.put(key, value); + } + } + } + + public boolean isAllowRecursiveKeys() { + return allowRecursiveKeys; + } + + public void setAllowRecursiveKeys(boolean allowRecursiveKeys) { + this.allowRecursiveKeys = allowRecursiveKeys; + } + + private static class CustomConstructorException extends ConstructorException { + public CustomConstructorException(String context, Mark contextMark, String problem, + Mark problemMark, Throwable cause) { + super(context, contextMark, problem, problemMark, cause); + } + } +} \ No newline at end of file diff --git a/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/Yaml.java b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/Yaml.java new file mode 100644 index 0000000..65ebdf8 --- /dev/null +++ b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/custom/Yaml.java @@ -0,0 +1,711 @@ +/** + * Copyright (c) 2008, http://www.snakeyaml.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.snakeyaml.custom; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.DumperOptions.FlowStyle; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.TypeDescription; +import org.yaml.snakeyaml.composer.Composer; +import org.yaml.snakeyaml.constructor.BaseConstructor; +import org.yaml.snakeyaml.emitter.Emitable; +import org.yaml.snakeyaml.emitter.Emitter; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.events.Event; +import org.yaml.snakeyaml.introspector.BeanAccess; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.parser.Parser; +import org.yaml.snakeyaml.parser.ParserImpl; +import org.yaml.snakeyaml.reader.StreamReader; +import org.yaml.snakeyaml.reader.UnicodeReader; +import org.yaml.snakeyaml.representer.Representer; +import org.yaml.snakeyaml.resolver.Resolver; +import org.yaml.snakeyaml.serializer.Serializer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.regex.Pattern; + +/** + * Public YAML interface. This class is not thread-safe. Which means that all the methods of the same + * instance can be called only by one thread. + * It is better to create an instance for every YAML stream. + * + * NOTE - This is a slight port of the Yaml class in SnakeYaml to specify the maxAliasesForCollections + * and also use CustomComposer + CustomConstructor. If this PR gets applied then we can remove it: + * https://bitbucket.org/asomov/snakeyaml/pull-requests/55/allow-configuration-for-preventing-billion/diff + */ +public class Yaml { + protected final Resolver resolver; + private String name; + protected BaseConstructor constructor; + protected Representer representer; + protected DumperOptions dumperOptions; + protected LoaderOptions loadingConfig; + private int maxAliasesForCollections = 50; + + + /** + * Create Yaml instance. + */ + public Yaml() { + this(new CustomConstructor(), new Representer(), new DumperOptions(), new LoaderOptions(), + new Resolver()); + } + + /** + * Create Yaml instance. + * + * @param dumperOptions DumperOptions to configure outgoing objects + */ + public Yaml(DumperOptions dumperOptions) { + this(new CustomConstructor(), new Representer(dumperOptions), dumperOptions); + } + + /** + * Create Yaml instance. + * + * @param loadingConfig LoadingConfig to control load behavior + */ + public Yaml(LoaderOptions loadingConfig) { + this(new CustomConstructor(), new Representer(), new DumperOptions(), loadingConfig); + } + + /** + * Create Yaml instance. + * + * @param representer Representer to emit outgoing objects + */ + public Yaml(Representer representer) { + this(new CustomConstructor(), representer); + } + + /** + * Create Yaml instance. + * + * @param constructor BaseConstructor to construct incoming documents + */ + public Yaml(BaseConstructor constructor) { + this(constructor, new Representer()); + } + + /** + * Create Yaml instance. + * + * @param constructor BaseConstructor to construct incoming documents + * @param representer Representer to emit outgoing objects + */ + public Yaml(BaseConstructor constructor, Representer representer) { + this(constructor, representer, initDumperOptions(representer)); + } + + private static DumperOptions initDumperOptions(Representer representer) { + DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setDefaultFlowStyle(representer.getDefaultFlowStyle()); + dumperOptions.setDefaultScalarStyle(representer.getDefaultScalarStyle()); + dumperOptions.setAllowReadOnlyProperties(representer.getPropertyUtils().isAllowReadOnlyProperties()); + dumperOptions.setTimeZone(representer.getTimeZone()); + return dumperOptions; + } + + /** + * Create Yaml instance. It is safe to create a few instances and use them + * in different Threads. + * + * @param representer Representer to emit outgoing objects + * @param dumperOptions DumperOptions to configure outgoing objects + */ + public Yaml(Representer representer, DumperOptions dumperOptions) { + this(new CustomConstructor(), representer, dumperOptions, new LoaderOptions(), new Resolver()); + } + + /** + * Create Yaml instance. It is safe to create a few instances and use them + * in different Threads. + * + * @param constructor BaseConstructor to construct incoming documents + * @param representer Representer to emit outgoing objects + * @param dumperOptions DumperOptions to configure outgoing objects + */ + public Yaml(BaseConstructor constructor, Representer representer, DumperOptions dumperOptions) { + this(constructor, representer, dumperOptions, new LoaderOptions(), new Resolver()); + } + + /** + * Create Yaml instance. It is safe to create a few instances and use them + * in different Threads. + * + * @param constructor BaseConstructor to construct incoming documents + * @param representer Representer to emit outgoing objects + * @param dumperOptions DumperOptions to configure outgoing objects + * @param loadingConfig LoadingConfig to control load behavior + */ + public Yaml(BaseConstructor constructor, Representer representer, DumperOptions dumperOptions, + LoaderOptions loadingConfig) { + this(constructor, representer, dumperOptions, loadingConfig, new Resolver()); + } + + /** + * Create Yaml instance. It is safe to create a few instances and use them + * in different Threads. + * + * @param constructor BaseConstructor to construct incoming documents + * @param representer Representer to emit outgoing objects + * @param dumperOptions DumperOptions to configure outgoing objects + * @param resolver Resolver to detect implicit type + */ + public Yaml(BaseConstructor constructor, Representer representer, DumperOptions dumperOptions, + Resolver resolver) { + this(constructor, representer, dumperOptions, new LoaderOptions(), resolver); + } + + public Yaml(BaseConstructor constructor, Representer representer, DumperOptions dumperOptions, + Resolver resolver, int maxAliasesForCollections) { + this(constructor, representer, dumperOptions, new LoaderOptions(), resolver); + this.maxAliasesForCollections = maxAliasesForCollections; + } + + /** + * Create Yaml instance. It is safe to create a few instances and use them + * in different Threads. + * + * @param constructor BaseConstructor to construct incoming documents + * @param representer Representer to emit outgoing objects + * @param dumperOptions DumperOptions to configure outgoing objects + * @param loadingConfig LoadingConfig to control load behavior + * @param resolver Resolver to detect implicit type + */ + public Yaml(BaseConstructor constructor, Representer representer, DumperOptions dumperOptions, + LoaderOptions loadingConfig, Resolver resolver) { + if (!constructor.isExplicitPropertyUtils()) { + constructor.setPropertyUtils(representer.getPropertyUtils()); + } else if (!representer.isExplicitPropertyUtils()) { + representer.setPropertyUtils(constructor.getPropertyUtils()); + } + this.constructor = constructor; + this.constructor.setAllowDuplicateKeys(loadingConfig.isAllowDuplicateKeys()); + this.constructor.setWrappedToRootException(loadingConfig.isWrappedToRootException()); + if (dumperOptions.getIndent() <= dumperOptions.getIndicatorIndent()) { + throw new YAMLException("Indicator indent must be smaller then indent."); + } + representer.setDefaultFlowStyle(dumperOptions.getDefaultFlowStyle()); + representer.setDefaultScalarStyle(dumperOptions.getDefaultScalarStyle()); + representer.getPropertyUtils() + .setAllowReadOnlyProperties(dumperOptions.isAllowReadOnlyProperties()); + representer.setTimeZone(dumperOptions.getTimeZone()); + this.representer = representer; + this.dumperOptions = dumperOptions; + this.loadingConfig = loadingConfig; + this.resolver = resolver; + this.name = "Yaml:" + System.identityHashCode(this); + } + + /** + * Serialize a Java object into a YAML String. + * + * @param data Java object to be Serialized to YAML + * @return YAML String + */ + public String dump(Object data) { + List<Object> list = new ArrayList<Object>(1); + list.add(data); + return dumpAll(list.iterator()); + } + + /** + * Produce the corresponding representation tree for a given Object. + * + * @param data instance to build the representation tree for + * @return representation tree + * @see <a href="http://yaml.org/spec/1.1/#id859333">Figure 3.1. Processing + * Overview</a> + */ + public Node represent(Object data) { + return representer.represent(data); + } + + /** + * Serialize a sequence of Java objects into a YAML String. + * + * @param data Iterator with Objects + * @return YAML String with all the objects in proper sequence + */ + public String dumpAll(Iterator<? extends Object> data) { + StringWriter buffer = new StringWriter(); + dumpAll(data, buffer, null); + return buffer.toString(); + } + + /** + * Serialize a Java object into a YAML stream. + * + * @param data Java object to be serialized to YAML + * @param output stream to write to + */ + public void dump(Object data, Writer output) { + List<Object> list = new ArrayList<Object>(1); + list.add(data); + dumpAll(list.iterator(), output, null); + } + + /** + * Serialize a sequence of Java objects into a YAML stream. + * + * @param data Iterator with Objects + * @param output stream to write to + */ + public void dumpAll(Iterator<? extends Object> data, Writer output) { + dumpAll(data, output, null); + } + + private void dumpAll(Iterator<? extends Object> data, Writer output, Tag rootTag) { + Serializer serializer = new Serializer(new Emitter(output, dumperOptions), resolver, + dumperOptions, rootTag); + try { + serializer.open(); + while (data.hasNext()) { + Node node = representer.represent(data.next()); + serializer.serialize(node); + } + serializer.close(); + } catch (IOException e) { + throw new YAMLException(e); + } + } + + /** + * <p> + * Serialize a Java object into a YAML string. Override the default root tag + * with <code>rootTag</code>. + * </p> + * + * <p> + * This method is similar to <code>Yaml.dump(data)</code> except that the + * root tag for the whole document is replaced with the given tag. This has + * two main uses. + * </p> + * + * <p> + * First, if the root tag is replaced with a standard YAML tag, such as + * <code>Tag.MAP</code>, then the object will be dumped as a map. The root + * tag will appear as <code>!!map</code>, or blank (implicit !!map). + * </p> + * + * <p> + * Second, if the root tag is replaced by a different custom tag, then the + * document appears to be a different type when loaded. For example, if an + * instance of MyClass is dumped with the tag !!YourClass, then it will be + * handled as an instance of YourClass when loaded. + * </p> + * + * @param data Java object to be serialized to YAML + * @param rootTag the tag for the whole YAML document. The tag should be Tag.MAP + * for a JavaBean to make the tag disappear (to use implicit tag + * !!map). If <code>null</code> is provided then the standard tag + * with the full class name is used. + * @param flowStyle flow style for the whole document. See Chapter 10. Collection + * Styles http://yaml.org/spec/1.1/#id930798. If + * <code>null</code> is provided then the flow style from + * DumperOptions is used. + * @return YAML String + */ + public String dumpAs(Object data, Tag rootTag, FlowStyle flowStyle) { + FlowStyle oldStyle = representer.getDefaultFlowStyle(); + if (flowStyle != null) { + representer.setDefaultFlowStyle(flowStyle); + } + List<Object> list = new ArrayList<Object>(1); + list.add(data); + StringWriter buffer = new StringWriter(); + dumpAll(list.iterator(), buffer, rootTag); + representer.setDefaultFlowStyle(oldStyle); + return buffer.toString(); + } + + /** + * <p> + * Serialize a Java object into a YAML string. Override the default root tag + * with <code>Tag.MAP</code>. + * </p> + * <p> + * This method is similar to <code>Yaml.dump(data)</code> except that the + * root tag for the whole document is replaced with <code>Tag.MAP</code> tag + * (implicit !!map). + * </p> + * <p> + * Block Mapping is used as the collection style. See 10.2.2. Block Mappings + * (http://yaml.org/spec/1.1/#id934537) + * </p> + * + * @param data Java object to be serialized to YAML + * @return YAML String + */ + public String dumpAsMap(Object data) { + return dumpAs(data, Tag.MAP, FlowStyle.BLOCK); + } + + /** + * Serialize the representation tree into Events. + * + * @param data representation tree + * @return Event list + * @see <a href="http://yaml.org/spec/1.1/#id859333">Processing Overview</a> + */ + public List<Event> serialize(Node data) { + SilentEmitter emitter = new SilentEmitter(); + Serializer serializer = new Serializer(emitter, resolver, dumperOptions, null); + try { + serializer.open(); + serializer.serialize(data); + serializer.close(); + } catch (IOException e) { + throw new YAMLException(e); + } + return emitter.getEvents(); + } + + private static class SilentEmitter implements Emitable { + private List<Event> events = new ArrayList<Event>(100); + + public List<Event> getEvents() { + return events; + } + + @Override + public void emit(Event event) throws IOException { + events.add(event); + } + } + + /** + * Parse the only YAML document in a String and produce the corresponding + * Java object. (Because the encoding in known BOM is not respected.) + * + * @param yaml YAML data to load from (BOM must not be present) + * @param <T> the class of the instance to be created + * @return parsed object + */ + @SuppressWarnings("unchecked") + public <T> T load(String yaml) { + return (T) loadFromReader(new StreamReader(yaml), Object.class); + } + + /** + * Parse the only YAML document in a stream and produce the corresponding + * Java object. + * + * @param io data to load from (BOM is respected to detect encoding and removed from the data) + * @param <T> the class of the instance to be created + * @return parsed object + */ + @SuppressWarnings("unchecked") + public <T> T load(InputStream io) { + return (T) loadFromReader(new StreamReader(new UnicodeReader(io)), Object.class); + } + + /** + * Parse the only YAML document in a stream and produce the corresponding + * Java object. + * + * @param io data to load from (BOM must not be present) + * @param <T> the class of the instance to be created + * @return parsed object + */ + @SuppressWarnings("unchecked") + public <T> T load(Reader io) { + return (T) loadFromReader(new StreamReader(io), Object.class); + } + + /** + * Parse the only YAML document in a stream and produce the corresponding + * Java object. + * + * @param <T> Class is defined by the second argument + * @param io data to load from (BOM must not be present) + * @param type Class of the object to be created + * @return parsed object + */ + @SuppressWarnings("unchecked") + public <T> T loadAs(Reader io, Class<T> type) { + return (T) loadFromReader(new StreamReader(io), type); + } + + /** + * Parse the only YAML document in a String and produce the corresponding + * Java object. (Because the encoding in known BOM is not respected.) + * + * @param <T> Class is defined by the second argument + * @param yaml YAML data to load from (BOM must not be present) + * @param type Class of the object to be created + * @return parsed object + */ + @SuppressWarnings("unchecked") + public <T> T loadAs(String yaml, Class<T> type) { + return (T) loadFromReader(new StreamReader(yaml), type); + } + + /** + * Parse the only YAML document in a stream and produce the corresponding + * Java object. + * + * @param <T> Class is defined by the second argument + * @param input data to load from (BOM is respected to detect encoding and removed from the data) + * @param type Class of the object to be created + * @return parsed object + */ + @SuppressWarnings("unchecked") + public <T> T loadAs(InputStream input, Class<T> type) { + return (T) loadFromReader(new StreamReader(new UnicodeReader(input)), type); + } + + private Object loadFromReader(StreamReader sreader, Class<?> type) { + Composer composer = new CustomComposer(new ParserImpl(sreader), resolver, maxAliasesForCollections); + constructor.setComposer(composer); + return constructor.getSingleData(type); + } + + /** + * Parse all YAML documents in the Reader and produce corresponding Java + * objects. The documents are parsed only when the iterator is invoked. + * + * @param yaml YAML data to load from (BOM must not be present) + * @return an Iterable over the parsed Java objects in this String in proper + * sequence + */ + public Iterable<Object> loadAll(Reader yaml) { + Composer composer = new CustomComposer(new ParserImpl(new StreamReader(yaml)), resolver, maxAliasesForCollections); + constructor.setComposer(composer); + Iterator<Object> result = new Iterator<Object>() { + @Override + public boolean hasNext() { + return constructor.checkData(); + } + + @Override + public Object next() { + return constructor.getData(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + return new YamlIterable(result); + } + + private static class YamlIterable implements Iterable<Object> { + private Iterator<Object> iterator; + + public YamlIterable(Iterator<Object> iterator) { + this.iterator = iterator; + } + + @Override + public Iterator<Object> iterator() { + return iterator; + } + } + + /** + * Parse all YAML documents in a String and produce corresponding Java + * objects. (Because the encoding in known BOM is not respected.) The + * documents are parsed only when the iterator is invoked. + * + * @param yaml YAML data to load from (BOM must not be present) + * @return an Iterable over the parsed Java objects in this String in proper + * sequence + */ + public Iterable<Object> loadAll(String yaml) { + return loadAll(new StringReader(yaml)); + } + + /** + * Parse all YAML documents in a stream and produce corresponding Java + * objects. The documents are parsed only when the iterator is invoked. + * + * @param yaml YAML data to load from (BOM is respected to detect encoding and removed from the data) + * @return an Iterable over the parsed Java objects in this stream in proper + * sequence + */ + public Iterable<Object> loadAll(InputStream yaml) { + return loadAll(new UnicodeReader(yaml)); + } + + /** + * Parse the first YAML document in a stream and produce the corresponding + * representation tree. (This is the opposite of the represent() method) + * + * @param yaml YAML document + * @return parsed root Node for the specified YAML document + * @see <a href="http://yaml.org/spec/1.1/#id859333">Figure 3.1. Processing + * Overview</a> + */ + public Node compose(Reader yaml) { + Composer composer = new CustomComposer(new ParserImpl(new StreamReader(yaml)), resolver, maxAliasesForCollections); + return composer.getSingleNode(); + } + + /** + * Parse all YAML documents in a stream and produce corresponding + * representation trees. + * + * @param yaml stream of YAML documents + * @return parsed root Nodes for all the specified YAML documents + * @see <a href="http://yaml.org/spec/1.1/#id859333">Processing Overview</a> + */ + public Iterable<Node> composeAll(Reader yaml) { + final Composer composer = new CustomComposer(new ParserImpl(new StreamReader(yaml)), resolver, maxAliasesForCollections); + Iterator<Node> result = new Iterator<Node>() { + @Override + public boolean hasNext() { + return composer.checkNode(); + } + + @Override + public Node next() { + Node node = composer.getNode(); + if (node != null) { + return node; + } else { + throw new NoSuchElementException("No Node is available."); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + return new NodeIterable(result); + } + + private static class NodeIterable implements Iterable<Node> { + private Iterator<Node> iterator; + + public NodeIterable(Iterator<Node> iterator) { + this.iterator = iterator; + } + + @Override + public Iterator<Node> iterator() { + return iterator; + } + } + + /** + * Add an implicit scalar detector. If an implicit scalar value matches the + * given regexp, the corresponding tag is assigned to the scalar. + * + * @param tag tag to assign to the node + * @param regexp regular expression to match against + * @param first a sequence of possible initial characters or null (which means + * any). + */ + public void addImplicitResolver(Tag tag, Pattern regexp, String first) { + resolver.addImplicitResolver(tag, regexp, first); + } + + @Override + public String toString() { + return name; + } + + /** + * Get a meaningful name. It simplifies debugging in a multi-threaded + * environment. If nothing is set explicitly the address of the instance is + * returned. + * + * @return human readable name + */ + public String getName() { + return name; + } + + /** + * Set a meaningful name to be shown in toString() + * + * @param name human readable name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Parse a YAML stream and produce parsing events. + * + * @param yaml YAML document(s) + * @return parsed events + * @see <a href="http://yaml.org/spec/1.1/#id859333">Processing Overview</a> + */ + public Iterable<Event> parse(Reader yaml) { + final Parser parser = new ParserImpl(new StreamReader(yaml)); + Iterator<Event> result = new Iterator<Event>() { + @Override + public boolean hasNext() { + return parser.peekEvent() != null; + } + + @Override + public Event next() { + Event event = parser.getEvent(); + if (event != null) { + return event; + } else { + throw new NoSuchElementException("No Event is available."); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + return new EventIterable(result); + } + + private static class EventIterable implements Iterable<Event> { + private Iterator<Event> iterator; + + public EventIterable(Iterator<Event> iterator) { + this.iterator = iterator; + } + + @Override + public Iterator<Event> iterator() { + return iterator; + } + } + + public void setBeanAccess(BeanAccess beanAccess) { + constructor.getPropertyUtils().setBeanAccess(beanAccess); + representer.getPropertyUtils().setBeanAccess(beanAccess); + } + + public void addTypeDescription(TypeDescription td) { + constructor.addTypeDescription(td); + representer.addTypeDescription(td); + } +} diff --git a/components/camel-snakeyaml/src/test/java/org/apache/camel/component/snakeyaml/SnakeYAMLDoSTest.java b/components/camel-snakeyaml/src/test/java/org/apache/camel/component/snakeyaml/SnakeYAMLDoSTest.java new file mode 100644 index 0000000..264b3da --- /dev/null +++ b/components/camel-snakeyaml/src/test/java/org/apache/camel/component/snakeyaml/SnakeYAMLDoSTest.java @@ -0,0 +1,156 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.snakeyaml; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import org.apache.camel.CamelExecutionException; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.component.snakeyaml.custom.Yaml; +import org.apache.camel.test.junit4.CamelTestSupport; +import org.junit.Test; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +public class SnakeYAMLDoSTest extends CamelTestSupport { + + @Test + public void testReadingDataFromFile() throws Exception { + + MockEndpoint mock = context.getEndpoint("mock:reverse", MockEndpoint.class); + assertNotNull(mock); + mock.expectedMessageCount(1); + + InputStream is = this.getClass().getClassLoader().getResourceAsStream("data.yaml"); + + ProducerTemplate template = context.createProducerTemplate(); + String result = template.requestBody("direct:back", is, String.class); + assertNotNull(result); + assertEquals("{name=Colm, location=Dublin}", result.trim()); + + mock.assertIsSatisfied(); + } + + @Test + public void testAliasExpansion() throws Exception { + + MockEndpoint mock = context.getEndpoint("mock:reverse", MockEndpoint.class); + assertNotNull(mock); + mock.expectedMessageCount(0); + + InputStream is = this.getClass().getClassLoader().getResourceAsStream("data-dos.yaml"); + + ProducerTemplate template = context.createProducerTemplate(); + try { + template.requestBody("direct:back", is, String.class); + fail("Failure expected on an alias expansion attack"); + } catch (CamelExecutionException ex) { + Throwable cause = ex.getCause(); + assertEquals("Number of aliases for non-scalar nodes exceeds the specified max=50", cause.getMessage()); + } + + mock.assertIsSatisfied(); + } + + @Test + public void testReferencesWithRecursiveKeysNotAllowedByDefault() throws Exception { + + MockEndpoint mock = context.getEndpoint("mock:reverse2", MockEndpoint.class); + assertNotNull(mock); + mock.expectedMessageCount(0); + + ProducerTemplate template = context.createProducerTemplate(); + try { + template.requestBody("direct:back2", createDump(30), String.class); + fail("Failure expected on an alias expansion attack"); + } catch (CamelExecutionException ex) { + Throwable cause = ex.getCause(); + assertEquals("Recursive key for mapping is detected but it is not configured to be allowed.", cause.getMessage()); + } + + mock.assertIsSatisfied(); + } + + // Taken from SnakeYaml test code + private String createDump(int size) { + Map<String, Object> root = new HashMap<>(); + Map<String, Object> s1, s2, t1, t2; + s1 = root; + s2 = new HashMap<>(); + /* + the time to parse grows very quickly + SIZE -> time to parse in seconds + 25 -> 1 + 26 -> 2 + 27 -> 3 + 28 -> 8 + 29 -> 13 + 30 -> 28 + 31 -> 52 + 32 -> 113 + 33 -> 245 + 34 -> 500 + */ + for (int i = 0; i < size; i++) { + + t1 = new HashMap<>(); + t2 = new HashMap<>(); + t1.put("foo", "1"); + t2.put("bar", "2"); + + s1.put("a", t1); + s1.put("b", t2); + s2.put("a", t1); + s2.put("b", t2); + + s1 = t1; + s2 = t2; + } + + // this is VERY BAD code + // the map has itself as a key (no idea why it may be used except of a DoS attack) + Map<Object, Object> f = new HashMap<>(); + f.put(f, "a"); + f.put("g", root); + + Yaml yaml = new Yaml(new SafeConstructor()); + return yaml.dump(f); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + SnakeYAMLDataFormat dataFormat = new SnakeYAMLDataFormat(); + dataFormat.setMaxAliasesForCollections(150); + + return new RouteBuilder() { + + @Override + public void configure() throws Exception { + from("direct:back") + .unmarshal(new SnakeYAMLDataFormat()) + .to("mock:reverse"); + + from("direct:back2") + .unmarshal(dataFormat) + .to("mock:reverse2"); + } + }; + } +} diff --git a/components/camel-snakeyaml/src/test/resources/data-dos.yaml b/components/camel-snakeyaml/src/test/resources/data-dos.yaml new file mode 100644 index 0000000..6747dd2 --- /dev/null +++ b/components/camel-snakeyaml/src/test/resources/data-dos.yaml @@ -0,0 +1,11 @@ +a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"] +b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a] +c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b] +d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c] +e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d] +f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e] +g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f] +h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g] +i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h] +name: *i +location: "Dublin" diff --git a/components/camel-snakeyaml/src/test/resources/data.yaml b/components/camel-snakeyaml/src/test/resources/data.yaml new file mode 100644 index 0000000..83c60e5 --- /dev/null +++ b/components/camel-snakeyaml/src/test/resources/data.yaml @@ -0,0 +1,2 @@ +name: "Colm" +location: "Dublin"