Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions conf/defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ topology.upstream.feedback.freq.secs: 10
topology.upstream.feedback.enable: false
topology.builtin.metrics.bucket.size.secs: 60
topology.fall.back.on.java.serialization: false
topology.fall.back.on.java.serialization.filter: "!org.apache.commons.collections.functors.*;!org.apache.commons.collections.comparators.*;!org.apache.commons.collections4.functors.*;!org.apache.commons.collections4.comparators.*;!org.apache.commons.beanutils.*;!org.apache.xalan.xsltc.trax.*;!com.sun.org.apache.xalan.internal.**;!com.sun.rowset.*;!com.sun.org.apache.rowset.internal.*;!com.mchange.v2.c3p0.**;!org.codehaus.groovy.runtime.ConvertedClosure;!org.codehaus.groovy.runtime.MethodClosure;maxbytes=10485760"
topology.worker.childopts: null
topology.worker.logwriter.childopts: "-Xmx64m"
topology.tick.tuple.freq.secs: null
Expand Down
2 changes: 2 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,8 @@ Storm uses Kryo for serializing tuple data between spouts and bolts. By default,

**Do not set `topology.fall.back.on.java.serialization` to `true` in production.** While topology submitters already run arbitrary code via their spouts and bolts, enabling the Java serialization fallback broadens the attack surface and may allow malicious data from external sources (e.g. message queues) to trigger unintended code execution during deserialization.

As defense in depth, the fallback bridge is constrained by `topology.fall.back.on.java.serialization.filter`, a [JEP-290](https://openjdk.org/jeps/290) serial-filter pattern applied whenever the bridge deserializes. `conf/defaults.yaml` sets a default pattern: a deny-list of well-known gadget namespaces plus `maxbytes=10485760`. An empty or unset value leaves the bridge unfiltered, as before. This reduces the impact of a misconfigured cluster.

For tuple encryption, use TLS-based transport encryption (`storm.messaging.netty.tls.enable`) instead of the deprecated `BlowfishTupleSerializer`, which uses a 64-bit block cipher vulnerable to birthday attacks.

### Log Cleanup
Expand Down
2 changes: 2 additions & 0 deletions docs/Serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ Beware that Java serialization is extremely expensive, both in terms of CPU cost

You can turn on/off the behavior to fall back on Java serialization by setting the `Config.TOPOLOGY_FALL_BACK_ON_JAVA_SERIALIZATION` config to true/false. The default value is false for security reasons.

When the fallback is enabled, the bridge can be constrained with `Config.TOPOLOGY_FALL_BACK_ON_JAVA_SERIALIZATION_FILTER`, a [JEP-290](https://openjdk.org/jeps/290) serial-filter pattern (e.g. `!org.apache.commons.collections4.functors.*;maxbytes=10485760`) applied to every `ObjectInputStream` the bridge uses for deserialization. The pattern is parsed when the serialization stack is created, so an invalid pattern fails worker setup with the config key in the error. `conf/defaults.yaml` carries a default deny-list of well-known gadget namespaces with a `maxbytes=10485760` limit; an empty or unset value leaves the bridge unfiltered, as before. Unlike a JVM-wide `-Djdk.serialFilter`, this filter is topology-scoped and also applies when the deserializer is constructed programmatically, e.g. in local mode.

### Tuple compression

For inter-worker (remote) traffic, Storm can optionally compress serialized tuples with [Zstandard](https://facebook.github.io/zstd/) before they are sent over the network. This is intended for one specific scenario: components that emit **large** payloads to a remote worker, where the bytes saved on the wire outweigh the CPU cost of compression. A good example is a spout that emits entire lines of text to a downstream bolt running on a different worker.
Expand Down
14 changes: 14 additions & 0 deletions storm-client/src/jvm/org/apache/storm/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,20 @@ public class Config extends HashMap<String, Object> {
*/
@IsBoolean
public static final String TOPOLOGY_FALL_BACK_ON_JAVA_SERIALIZATION = "topology.fall.back.on.java.serialization";
/**
* Optional <a href="https://openjdk.org/jeps/290">JEP-290</a> serial-filter pattern applied to the
* Java-serialization fallback bridge that {@link #TOPOLOGY_FALL_BACK_ON_JAVA_SERIALIZATION} enables for
* unregistered classes. When set to a non-empty pattern, it is parsed once at kryo construction and installed
* on every {@code ObjectInputStream} used to deserialize fallback values, so stream classes rejected by the
* filter are neither instantiated nor have their {@code readObject} logic invoked. {@code conf/defaults.yaml}
* sets a default gadget deny-list with a {@code maxbytes=10485760} limit; an empty or unset value leaves the
* bridge unfiltered, as before. An invalid pattern fails worker startup with a clear error. Note: Unlike a
* JVM-wide {@code jdk.serialFilter}, this is topology-scoped and also applies when the deserializer is built
* programmatically, e.g. local mode. Example deny-list: {@code !org.apache.commons.collections4.functors.*}.
*/
@IsString
public static final String TOPOLOGY_FALL_BACK_ON_JAVA_SERIALIZATION_FILTER =
"topology.fall.back.on.java.serialization.filter";
/**
* Topology-specific options for the worker child process. This is used in addition to WORKER_CHILDOPTS.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.Serializer;
import com.esotericsoftware.kryo.util.Util;
import java.io.ObjectInputFilter;
import java.util.Map;
import org.apache.storm.Config;
import org.slf4j.Logger;
Expand All @@ -29,9 +30,27 @@ public Kryo getKryo(Map<String, Object> conf) {
KryoSerializableDefault k = new KryoSerializableDefault();
k.setRegistrationRequired(!((Boolean) conf.get(Config.TOPOLOGY_FALL_BACK_ON_JAVA_SERIALIZATION)));
k.setReferences(false);
k.setJavaSerializationFilter(getJavaSerializationFilter(conf));
return k;
}

/**
* Parses the pattern once at kryo construction so an invalid pattern fails worker setup with a clear error
* instead of failing per-tuple on the read path. Returns null when the key is unset or empty (no filter).
*/
private static ObjectInputFilter getJavaSerializationFilter(Map<String, Object> conf) {
String filterSpec = (String) conf.get(Config.TOPOLOGY_FALL_BACK_ON_JAVA_SERIALIZATION_FILTER);
if (filterSpec == null || filterSpec.isEmpty()) {
return null;
}
try {
return ObjectInputFilter.Config.createFilter(filterSpec);
} catch (IllegalArgumentException e) {
throw new RuntimeException("Invalid " + Config.TOPOLOGY_FALL_BACK_ON_JAVA_SERIALIZATION_FILTER
+ " pattern: \"" + filterSpec + "\"", e);
}
}

@Override
public void preRegister(Kryo k, Map<String, Object> conf) {
}
Expand All @@ -47,6 +66,11 @@ public void postDecorate(Kryo k, Map<String, Object> conf) {

public static class KryoSerializableDefault extends Kryo {
boolean override = false;
private ObjectInputFilter javaSerializationFilter;

public void setJavaSerializationFilter(ObjectInputFilter filter) {
this.javaSerializationFilter = filter;
}

public void overrideDefault(boolean value) {
override = value;
Expand All @@ -61,7 +85,7 @@ public Serializer getDefaultSerializer(Class type) {
Util.className(type), Util.className(type)
);

return new SerializableSerializer();
return new SerializableSerializer(javaSerializationFilter);
} else {
return super.getDefaultSerializer(type);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,29 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;


public class SerializableSerializer extends Serializer<Object> {

/**
* Optional JEP-290 filter applied to each ObjectInputStream used for deserialization (null means unfiltered,
* as before). The filter itself is created once from
* {@link org.apache.storm.Config#TOPOLOGY_FALL_BACK_ON_JAVA_SERIALIZATION_FILTER} by {@link DefaultKryoFactory};
* instances returned by {@link ObjectInputFilter.Config#createFilter} are immutable and safe to share across streams.
*/
private final ObjectInputFilter serialFilter;

public SerializableSerializer() {
this(null);
}

public SerializableSerializer(ObjectInputFilter serialFilter) {
this.serialFilter = serialFilter;
}

@Override
public void write(Kryo kryo, Output output, Object object) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Expand All @@ -48,6 +65,9 @@ public Object read(Kryo kryo, Input input, Class c) {
ByteArrayInputStream bis = new ByteArrayInputStream(ser);
try {
ObjectInputStream ois = new ObjectInputStream(bis);
if (serialFilter != null) {
ois.setObjectInputFilter(serialFilter);
}
return ois.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* 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 com.mchange.v2.c3p0.impl;

import java.io.Serializable;

/**
* Placeholder under com.mchange.v2.c3p0.impl; sits in a subpackage so the tests cover the difference between pkg.* and pkg.** entries.
*/
public class SimulatedGadget implements Serializable {

private static final long serialVersionUID = 1L;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* 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.commons.collections.comparators;

import java.io.Serializable;

/**
* Placeholder in the commons-collections comparators package; TransformingComparator gadgets live here, not in functors.
*/
public class SimulatedGadget implements Serializable {

private static final long serialVersionUID = 1L;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* 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.commons.collections.functors;

import java.io.Serializable;

/**
* Placeholder class in the commons-collections functors package so the filter tests deny a class that actually loads (the gadget
* here is InvokerTransformer).
*/
public class SimulatedGadget implements Serializable {

private static final long serialVersionUID = 1L;
}
Loading
Loading