diff --git a/common/common-rest/src/main/java/com/fasterxml/jackson/core/base/DoSFix.java b/common/common-rest/src/main/java/com/fasterxml/jackson/core/base/DoSFix.java new file mode 100644 index 00000000000..6c46eca15c7 --- /dev/null +++ b/common/common-rest/src/main/java/com/fasterxml/jackson/core/base/DoSFix.java @@ -0,0 +1,151 @@ +/* + * 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.fasterxml.jackson.core.base; + +import org.apache.servicecomb.foundation.common.utils.JvmUtils; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper; +import com.fasterxml.jackson.core.json.ReaderBasedJsonParser; +import com.fasterxml.jackson.core.json.UTF8StreamJsonParser; +import com.fasterxml.jackson.databind.MappingJsonFactory; +import com.netflix.config.DynamicPropertyFactory; + +import javassist.CannotCompileException; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtMethod; +import javassist.LoaderClassPath; +import javassist.NotFoundException; + +/** + * will be deleted after jackson fix the DoS problem: + * https://github.com/FasterXML/jackson-databind/issues/2157 + */ +public class DoSFix { + private static final String SUFFIX = "Fixed"; + + private static boolean enabled = DynamicPropertyFactory.getInstance() + .getBooleanProperty("servicecomb.jackson.fix.DoS.enabled", true).get(); + + private static boolean fixed; + + private static Class mappingJsonFactoryClass; + + public static synchronized void init() { + if (fixed || !enabled) { + return; + } + + fix(); + } + + public static JsonFactory createJsonFactory() { + try { + return (JsonFactory) mappingJsonFactoryClass.newInstance(); + } catch (Throwable e) { + throw new IllegalStateException("Failed to create JsonFactory.", e); + } + } + + private static void fix() { + try { + ClassLoader classLoader = JvmUtils.correctClassLoader(DoSFix.class.getClassLoader()); + ClassPool pool = new ClassPool(ClassPool.getDefault()); + pool.appendClassPath(new LoaderClassPath(classLoader)); + + fixParserBase(classLoader, pool); + fixReaderParser(classLoader, pool); + fixStreamParser(classLoader, pool); + fixByteSourceJsonBootstrapper(classLoader, pool); + + CtClass ctJsonFactoryFixedClass = fixJsonFactory(classLoader, pool); + fixMappingJsonFactoryClass(classLoader, pool, ctJsonFactoryFixedClass); + + fixed = true; + } catch (Throwable e) { + throw new IllegalStateException( + "Failed to fix jackson DoS bug.", + e); + } + } + + private static void fixMappingJsonFactoryClass(ClassLoader classLoader, ClassPool pool, + CtClass ctJsonFactoryFixedClass) throws NotFoundException, CannotCompileException { + CtClass ctMappingJsonFactoryClass = pool + .getAndRename(MappingJsonFactory.class.getName(), MappingJsonFactory.class.getName() + SUFFIX); + ctMappingJsonFactoryClass.setSuperclass(ctJsonFactoryFixedClass); + mappingJsonFactoryClass = ctMappingJsonFactoryClass.toClass(classLoader, null); + } + + private static CtClass fixJsonFactory(ClassLoader classLoader, ClassPool pool) + throws NotFoundException, CannotCompileException { + CtClass ctJsonFactoryClass = pool.getCtClass(JsonFactory.class.getName()); + CtClass ctJsonFactoryFixedClass = pool.makeClass(JsonFactory.class.getName() + SUFFIX); + ctJsonFactoryFixedClass.setSuperclass(ctJsonFactoryClass); + for (CtMethod ctMethod : ctJsonFactoryClass.getDeclaredMethods()) { + if (ctMethod.getName().equals("_createParser")) { + ctJsonFactoryFixedClass.addMethod(new CtMethod(ctMethod, ctJsonFactoryFixedClass, null)); + } + } + ctJsonFactoryFixedClass + .replaceClassName(ReaderBasedJsonParser.class.getName(), ReaderBasedJsonParser.class.getName() + SUFFIX); + ctJsonFactoryFixedClass + .replaceClassName(UTF8StreamJsonParser.class.getName(), UTF8StreamJsonParser.class.getName() + SUFFIX); + ctJsonFactoryFixedClass.replaceClassName(ByteSourceJsonBootstrapper.class.getName(), + ByteSourceJsonBootstrapper.class.getName() + SUFFIX); + ctJsonFactoryFixedClass.toClass(classLoader, null); + + return ctJsonFactoryFixedClass; + } + + private static void fixByteSourceJsonBootstrapper(ClassLoader classLoader, ClassPool pool) + throws NotFoundException, CannotCompileException { + CtClass ctByteSourceJsonBootstrapper = pool + .getAndRename(ByteSourceJsonBootstrapper.class.getName(), ByteSourceJsonBootstrapper.class.getName() + SUFFIX); + ctByteSourceJsonBootstrapper + .replaceClassName(UTF8StreamJsonParser.class.getName(), UTF8StreamJsonParser.class.getName() + SUFFIX); + ctByteSourceJsonBootstrapper + .replaceClassName(ReaderBasedJsonParser.class.getName(), ReaderBasedJsonParser.class.getName() + SUFFIX); + ctByteSourceJsonBootstrapper.toClass(classLoader, null); + } + + private static void fixStreamParser(ClassLoader classLoader, ClassPool pool) + throws NotFoundException, CannotCompileException { + CtClass ctStreamClass = pool + .getAndRename(UTF8StreamJsonParser.class.getName(), UTF8StreamJsonParser.class.getName() + SUFFIX); + ctStreamClass.replaceClassName(ParserBase.class.getName(), ParserBase.class.getName() + SUFFIX); + ctStreamClass.toClass(classLoader, null); + } + + private static void fixReaderParser(ClassLoader classLoader, ClassPool pool) + throws NotFoundException, CannotCompileException { + CtClass ctReaderClass = pool + .getAndRename(ReaderBasedJsonParser.class.getName(), ReaderBasedJsonParser.class.getName() + SUFFIX); + ctReaderClass.replaceClassName(ParserBase.class.getName(), ParserBase.class.getName() + SUFFIX); + ctReaderClass.toClass(classLoader, null); + } + + private static void fixParserBase(ClassLoader classLoader, ClassPool pool) + throws NotFoundException, CannotCompileException { + CtMethod ctMethodFixed = pool.get(DoSParserFixed.class.getName()).getDeclaredMethod("_parseSlowInt"); + CtClass baseClass = pool.getAndRename(ParserBase.class.getName(), ParserBase.class.getName() + SUFFIX); + baseClass.removeMethod(baseClass.getDeclaredMethod("_parseSlowInt")); + baseClass.addMethod(new CtMethod(ctMethodFixed, baseClass, null)); + baseClass.toClass(classLoader, null); + } +} diff --git a/common/common-rest/src/main/java/com/fasterxml/jackson/core/base/DoSParserFixed.java b/common/common-rest/src/main/java/com/fasterxml/jackson/core/base/DoSParserFixed.java new file mode 100644 index 00000000000..71fbdc675b0 --- /dev/null +++ b/common/common-rest/src/main/java/com/fasterxml/jackson/core/base/DoSParserFixed.java @@ -0,0 +1,76 @@ +/* + * 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.fasterxml.jackson.core.base; + +import java.io.IOException; +import java.io.Reader; +import java.math.BigInteger; + +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.io.IOContext; +import com.fasterxml.jackson.core.io.NumberInput; +import com.fasterxml.jackson.core.json.ReaderBasedJsonParser; +import com.fasterxml.jackson.core.sym.CharsToNameCanonicalizer; + +/** + * will not be use directly + * just get _parseSlowInt/_parseSlowFloat bytecode and replace to ParserBase + */ +public abstract class DoSParserFixed extends ReaderBasedJsonParser { + public DoSParserFixed(IOContext ctxt, int features, Reader r, + ObjectCodec codec, CharsToNameCanonicalizer st, + char[] inputBuffer, int start, int end, boolean bufferRecyclable) { + super(ctxt, features, r, codec, st, inputBuffer, start, end, bufferRecyclable); + } + + private void _parseSlowInt(int expType) throws IOException { + String numStr = _textBuffer.contentsAsString(); + try { + int len = _intLength; + char[] buf = _textBuffer.getTextBuffer(); + int offset = _textBuffer.getTextOffset(); + if (_numberNegative) { + ++offset; + } + // Some long cases still... + if (NumberInput.inLongRange(buf, offset, len, _numberNegative)) { + // Probably faster to construct a String, call parse, than to use BigInteger + _numberLong = Long.parseLong(numStr); + _numTypesValid = NR_LONG; + } else { + // nope, need the heavy guns... (rare case) + + // *** fix DoS attack begin *** + if (NR_DOUBLE == expType || NR_FLOAT == expType) { + _numberDouble = Double.parseDouble(numStr); + _numTypesValid = NR_DOUBLE; + return; + } + if (NR_BIGINT != expType) { + throw new NumberFormatException("invalid numeric value '" + numStr + "'"); + } + // *** fix DoS attack end *** + + _numberBigInt = new BigInteger(numStr); + _numTypesValid = NR_BIGINT; + } + } catch (NumberFormatException nex) { + // Can this ever occur? Due to overflow, maybe? + _wrapError("Malformed numeric value '" + numStr + "'", nex); + } + } +} diff --git a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/AbstractRestObjectMapper.java b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/AbstractRestObjectMapper.java index 0ca5fa6a019..1f64d3d0543 100644 --- a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/AbstractRestObjectMapper.java +++ b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/AbstractRestObjectMapper.java @@ -17,10 +17,15 @@ package org.apache.servicecomb.common.rest.codec; +import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.ObjectMapper; public abstract class AbstractRestObjectMapper extends ObjectMapper { private static final long serialVersionUID = 189026839992490564L; + public AbstractRestObjectMapper(JsonFactory jsonFactory) { + super(jsonFactory); + } + abstract public String convertToString(Object value) throws Exception; } diff --git a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/RestObjectMapper.java b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/RestObjectMapper.java index 8f20acbfc30..ae1a45f7fcf 100644 --- a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/RestObjectMapper.java +++ b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/RestObjectMapper.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser.Feature; +import com.fasterxml.jackson.core.base.DoSFix; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonSerializer; @@ -34,6 +35,10 @@ import io.vertx.core.json.JsonObject; public class RestObjectMapper extends AbstractRestObjectMapper { + static { + DoSFix.init(); + } + private static class JsonObjectSerializer extends JsonSerializer { @Override public void serialize(JsonObject value, JsonGenerator jgen, SerializerProvider provider) throws IOException { @@ -47,6 +52,8 @@ public void serialize(JsonObject value, JsonGenerator jgen, SerializerProvider p @SuppressWarnings("deprecation") public RestObjectMapper() { + super(DoSFix.createJsonFactory()); + // swagger中要求date使用ISO8601格式传递,这里与之做了功能绑定,这在cse中是没有问题的 setDateFormat(new com.fasterxml.jackson.databind.util.ISO8601DateFormat() { private static final long serialVersionUID = 7798938088541203312L; diff --git a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/produce/ProduceProcessorManager.java b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/produce/ProduceProcessorManager.java index 255b35d770a..6bd5babc056 100644 --- a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/produce/ProduceProcessorManager.java +++ b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/codec/produce/ProduceProcessorManager.java @@ -20,6 +20,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; + import javax.ws.rs.core.MediaType; import org.apache.servicecomb.foundation.common.RegisterManager; @@ -47,8 +48,9 @@ private ProduceProcessorManager() { super(NAME); Set set = new HashSet<>(); produceProcessor.forEach(processor -> { - if (set.add(processor.getName())) + if (set.add(processor.getName())) { register(processor.getName(), processor); + } }); } } diff --git a/common/common-rest/src/test/java/org/apache/servicecomb/common/rest/codec/fix/TestDoSFix.java b/common/common-rest/src/test/java/org/apache/servicecomb/common/rest/codec/fix/TestDoSFix.java new file mode 100644 index 00000000000..baff89771f6 --- /dev/null +++ b/common/common-rest/src/test/java/org/apache/servicecomb/common/rest/codec/fix/TestDoSFix.java @@ -0,0 +1,246 @@ +/* + * 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.servicecomb.common.rest.codec.fix; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.util.concurrent.Callable; + +import org.apache.servicecomb.common.rest.codec.RestObjectMapper; +import org.apache.servicecomb.foundation.test.scaffolding.model.Color; +import org.junit.Assert; +import org.junit.Test; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.google.common.base.Strings; + +public class TestDoSFix { + static ObjectMapper mapper = new RestObjectMapper(); + + static String invalidNum = Strings.repeat("9", 100_0000); + + static String invalidStr = "\"" + invalidNum + "\""; + + static String invalidArrNum = "[" + invalidNum + "]"; + + static String invalidArrStr = "[\"" + invalidNum + "\"]"; + + public static class Model { + public Color color; + + public char cValue; + + public Character cObjValue; + + public byte bValue; + + public Byte bObjValue; + + public short sValue; + + public Short sObjValue; + + public int iValue; + + public Integer iObjValue; + + public long lValue; + + public Long lObjValue; + + public float fValue; + + public Float fObjValue; + + public double dValue; + + public Double dObjValue; + } + + void fastFail(Callable callable, Class eCls) { + long start = System.currentTimeMillis(); + try { + Object ret = callable.call(); + Assert.fail("expect failed, but succes to be " + ret); + } catch (AssertionError e) { + throw e; + } catch (Throwable e) { + if (eCls != e.getClass()) { + e.printStackTrace(); + } + Assert.assertEquals(eCls, e.getClass()); + } + + long time = System.currentTimeMillis() - start; + Assert.assertTrue("did not fix DoS problem, time:" + time, time < 1000); + } + + void fastFail(String input, Class cls, Class eCls) { + fastFail(() -> mapper.readValue(input, cls), eCls); + + fastFail(() -> mapper.readValue(new ByteArrayInputStream(input.getBytes()), cls), eCls); + } + + void batFastFail(Class cls, Class e1, Class e2) { + fastFail(invalidNum, cls, e1); + fastFail(invalidStr, cls, e2); + fastFail(invalidArrNum, cls, e1); + fastFail(invalidArrStr, cls, e2); + } + + void batFastFail(Class cls) { + batFastFail(cls, JsonParseException.class, InvalidFormatException.class); + } + + void batFastFail(String fieldName, Class e1, Class e2) { + fastFail("{\"" + fieldName + "\":" + invalidNum + "}", Model.class, e1); + fastFail("{\"" + fieldName + "\":\"" + invalidNum + "\"}", Model.class, e2); + fastFail("{\"" + fieldName + "\":[" + invalidNum + "]}", Model.class, e1); + fastFail("{\"" + fieldName + "\":[\"" + invalidNum + "\"]}", Model.class, e2); + } + + void batFastFail(String fieldName) { + batFastFail(fieldName, JsonMappingException.class, InvalidFormatException.class); + } + + @Test + public void testEnum() { + batFastFail(Color.class); + batFastFail("color"); + } + + @Test + public void testChar() { + batFastFail(char.class, JsonParseException.class, MismatchedInputException.class); + batFastFail(Character.class, JsonParseException.class, MismatchedInputException.class); + + batFastFail("cValue", JsonMappingException.class, MismatchedInputException.class); + batFastFail("cObjValue", JsonMappingException.class, MismatchedInputException.class); + } + + @Test + public void testByte() { + batFastFail(byte.class); + batFastFail(Byte.class); + + batFastFail("bValue"); + batFastFail("bObjValue"); + } + + @Test + public void testShort() { + batFastFail(short.class); + batFastFail(Short.class); + + batFastFail("sValue"); + batFastFail("sObjValue"); + } + + @Test + public void testInt() { + batFastFail(int.class); + batFastFail(Integer.class); + + batFastFail("iValue"); + batFastFail("iObjValue"); + } + + @Test + public void testLong() { + batFastFail(long.class); + batFastFail(Long.class); + + batFastFail("lValue"); + batFastFail("lObjValue"); + } + + Object fastSucc(Callable callable) { + long start = System.currentTimeMillis(); + try { + Object ret = callable.call(); + Assert.assertTrue(System.currentTimeMillis() - start < 1000); + return ret; + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } + + Object fastSucc(String input, Class cls) { + return fastSucc(() -> mapper.readValue(input, cls)); + } + + Object fastSucc(InputStream input, Class cls) { + return fastSucc(() -> { + input.reset(); + return mapper.readValue(input, cls); + }); + } + + void batFastSucc(Class cls, Object expected) { + Assert.assertEquals(expected, fastSucc(invalidNum, cls)); + Assert.assertEquals(expected, fastSucc(new ByteArrayInputStream(invalidNum.getBytes()), cls)); + + Assert.assertEquals(expected, fastSucc(invalidStr, cls)); + Assert.assertEquals(expected, fastSucc(new ByteArrayInputStream(invalidStr.getBytes()), cls)); + + Assert.assertEquals(expected, fastSucc(invalidArrNum, cls)); + Assert.assertEquals(expected, fastSucc(new ByteArrayInputStream(invalidArrNum.getBytes()), cls)); + + Assert.assertEquals(expected, fastSucc(invalidArrStr, cls)); + Assert.assertEquals(expected, fastSucc(new ByteArrayInputStream(invalidArrStr.getBytes()), cls)); + } + + void checkField(Model model, String fieldName, Object expected) { + try { + Field field = Model.class.getField(fieldName); + Object value = field.get(model); + Assert.assertEquals(expected, value); + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } + + void batFastSucc(String fieldName, Object expected) { + checkField((Model) fastSucc("{\"" + fieldName + "\":" + invalidNum + "}", Model.class), fieldName, expected); + checkField((Model) fastSucc("{\"" + fieldName + "\":\"" + invalidNum + "\"}", Model.class), fieldName, expected); + checkField((Model) fastSucc("{\"" + fieldName + "\":[" + invalidNum + "]}", Model.class), fieldName, expected); + checkField((Model) fastSucc("{\"" + fieldName + "\":[\"" + invalidNum + "\"]}", Model.class), fieldName, expected); + } + + @Test + public void testFloat() { + batFastSucc(float.class, Float.POSITIVE_INFINITY); + batFastSucc(Float.class, Float.POSITIVE_INFINITY); + + batFastSucc("fValue", Float.POSITIVE_INFINITY); + batFastSucc("fObjValue", Float.POSITIVE_INFINITY); + } + + @Test + public void testDouble() { + batFastSucc(double.class, Double.POSITIVE_INFINITY); + batFastSucc(Double.class, Double.POSITIVE_INFINITY); + + batFastSucc("dValue", Double.POSITIVE_INFINITY); + batFastSucc("dObjValue", Double.POSITIVE_INFINITY); + } +}