-
Notifications
You must be signed in to change notification settings - Fork 118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
code to handle FasterXML/jackson-databind/issues/2141 #84
Conversation
NumberFormat formatter = new DecimalFormat("0.0E0"); | ||
throw new JsonParseException(context.getParser(), | ||
String.format("Value of BigDecimal (%s) not within range to be converted to Duration", | ||
formatter.format(value))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this use string
rather than reformatting the parsed value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the string is a 1,000,000 characters long, I would assume printing it might be more trouble than what its worth, -- eg the test case where I append "1000000000" a thousand times would end up with a 1mb long exception message.
I can just not print the string and just say its not within the expected range.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it'd be better to just include length of String as presumably this should only be hit for attack, or some other use case in which input is not legitimate. So printing that value does not make sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DoS is not coupled to the length of the string, in fact the problem is that short input strings can cause the failure, by representing very large numbers.
Is there a potential for this same problem -- |
@@ -48,22 +58,35 @@ private DurationDeserializer() | |||
@Override | |||
public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException | |||
{ | |||
String string = parser.getText().trim(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will allocate twice as much than before. I would keep the string inside the switch statement, or alternatively see if there is a way to get the length from the parser without allocating a new string
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can try getTextLength() -- I don't know what the implementation of that is however (eg, if it allocates memory or not). I'll have to look around later today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed this code entirely.
public void testDeserializationWithTypeInfoAndStringTooLarge03() throws Exception | ||
{ | ||
// Build a big string by hand | ||
String number = "0000000000000000000000000"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would using an actual number rather than 0 be more resilient to future code changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can, but how many zeroes would be reasonable 100?
I believe the current java Instant has a max length of ~30ish digits.
It is an actual number, however, its a number "1" followed by 1_000 x "00000000000" appended.
It definitely causes the JVM to hang without the parser.text.length() check above.
@toddjonker -- Yes, I would assume the same thing would happen for other areas. |
long seconds = value.longValue(); | ||
int nanoseconds = DecimalUtils.extractNanosecondDecimal(value, seconds); | ||
return Duration.ofSeconds(seconds, nanoseconds); | ||
|
||
case JsonTokenId.ID_NUMBER_INT: | ||
if(string.length() > INSTANT_MAX_STRING_LEN) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the attack also possible via BigInteger
, or just BigDecimal
? I am thinking that (as per earlier comments by others) for common case there would be unnecessary allocation of another String, for the common case of input being relatively short.
And for special case of binary formats there would also be conversion from binary number into String.
So the extra checks would mostly make sense if use of BigInteger
is also similarly dangerous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just BigDecimal
, because BigDecimal.longValue()
(and intValue()
and scale()
and some others) will consume unbounded resources even given short string representations of the decimal value. It's the conversion to an integer that's the problem.
The input-string length checks in this PR are unrelated to the reported DoS vector. IMO they should be submitted and considered separately, because they are muddying the analysis here by conflating independent concerns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have removed all the string length checking entirely and it only contains the code to handle exponential numbers.
QQ @cowtowncoder - Is there a specific form / CLA to fill out for this contribution? |
Guards against numbers causing CPU or OOM issues when deserializing large exponential numbers into Instant or Duration by either: - Scientific notation too large (eg 10000e100000)
Please take a look at my comment on FasterXML/jackson-databind#2141 (comment) Bound checking is necessary but not sufficient as the problem also happens for large negative exponents, e.g. |
@abracadv8 Basic Jackson CLA from https://github.com/FasterXML/jackson/blob/master/contributor-agreement.pdf works, if you haven't sent one (and are not covered by one of existing Corporate CLAs we have received) |
There is a test for |
I think it should possible to guard against small numbers by checking the scale. |
Sent. I'm contributing this under my personal time/resources, but I'm going to submit it to my company and fill out a second if they approve. |
@arteam That's what I am thinking -- magnitude of scale could be limited to, say, max 50 (that is, -50 <= scale <= 50) and that would cover ridiculously big/small values already, yet pose no significant DoS capability. Also, on related note, similar problem affects |
One other question: since we should be able to detect "bigger than max" and "smaller than min" values for conversion cases, should we:
For underflow ("too small"), I assume we could just coerce it to |
@cowtowncoder just my take: I would write a test that validates the behavior of inputs just above "too big", and just below "too small" prior to the change. Run these tests with the existing code prior to the change, and then make the fix to keep the same behavior, thus preventing unexpected behavior changes, regardless what is the more correct behavior you come up with in a clean design. Why? This library is already super popular, and it is hard to tell how existing code depends on that edge case. Making a change in behavior in a minor version is not justified. |
@@ -52,6 +58,13 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t | |||
{ | |||
case JsonTokenId.ID_NUMBER_FLOAT: | |||
BigDecimal value = parser.getDecimalValue(); | |||
// If the decimal isnt within the bounds of a float, bail out |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't "float" be "Instant"? Also, misspelling of "isn't".
Also might be worth pointing out in comment that UPDATE: they do not.Duration
and Instant
share the same range.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to // If the decimal isn't within the bounds of a duration, bail out
since a duration has different bounds than an instant.
// If the decimal isnt within the bounds of an Instant, bail out | ||
if(value.compareTo(INSTANT_MAX) > 0 || | ||
value.compareTo(INSTANT_MIN) < 0) { | ||
throw new JsonParseException(context.getParser(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Today, this method throws DateTimeException
when the given value is out of bounds, so we should throw that here, else this is a backwards-incompatible change.
This test case:
@Test // (expected = DateTimeException.class) // Disabled to provoke failure
public void testDeserializationAsFloat04() throws Exception
{
Instant date = Instant.MAX;
String customInstant = (date.getEpochSecond() + 1) + ".0";
MAPPER.readValue(customInstant, Instant.class);
}
results in:
java.time.DateTimeException: Instant exceeds minimum or maximum instant
at java.time.Instant.create(Instant.java:411)
at java.time.Instant.ofEpochSecond(Instant.java:330)
at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.lambda$static$1(InstantDeserializer.java:66)
at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer._fromDecimal(InstantDeserializer.java:284)
at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:171)
at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:50)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3004)
at com.fasterxml.jackson.datatype.jsr310.TestInstantSerialization.testDeserializationAsFloat04(TestInstantSerialization.java:244)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I swapped the checks to throw throw new DateTimeException("Instant exceeds minimum or maximum instant");
and throw new DateTimeException("Instant exceeds minimum or maximum Duration");
respectively.
I updated the bounds for duration to be the following, although I am not 100% certain it is correct with regards to minimum:
private static final BigDecimal DURATION_MAX = new BigDecimal(
Long.MAX_VALUE + "." + java.time.Instant.MAX.getNano());
private static final BigDecimal DURATION_MIN = new BigDecimal(
Long.MIN_VALUE + "." + java.time.Instant.MIN.getNano());
if(value.compareTo(INSTANT_MAX) > 0 || | ||
value.compareTo(INSTANT_MIN) < 0) { | ||
throw new JsonParseException(context.getParser(), | ||
"Value of Float too large to be converted to Duration"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out this isn't the correct boundary: Duration
can hold a number-of-seconds up to Long.MAX_VALUE
, which is larger than Instant.MAX.getEpochSecond()
.
For larger values, Jackson currently has the troubling behavior of BigDecimal.longValue()
. This test passes:
@Test
public void testDeserializationAsFloat05() throws Exception
{
String customInstant = new BigInteger(Long.toString(Long.MAX_VALUE)).add(BigInteger.ONE) + ".0";
Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
.readValue(customInstant);
assertEquals(Long.MIN_VALUE, value.getSeconds()); // The Duration is negative!
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For reference on behavior of similar overflow, Duration.of(Long.MAX_VALUE, ChronoUnit.MINUTES)
throws:
java.lang.ArithmeticException: long overflow
while Duration.parse("PT" + Long.MAX_VALUE + "0S")
-- note the added 0
to force overflow -- throws:
java.time.format.DateTimeParseException: Text cannot be parsed to a Duration: seconds
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Swapped for the following check, although I am not 100% sure about the correct min value:
private static final BigDecimal DURATION_MAX = new BigDecimal(
Long.MAX_VALUE + "." + java.time.Instant.MAX.getNano());
private static final BigDecimal DURATION_MIN = new BigDecimal(
Long.MIN_VALUE + "." + java.time.Instant.MIN.getNano());
All the test cases, including yours above now throw new DateTimeException( "Instant exceeds minimum or maximum Duration")
…ing exception messages - Changed JsonParseException to DateTimeException where needed - Extended bounds of min and max Duration to be Long.MAX and Long.MIN - Adjusting DateTimeException message to match original for Instant - Adjusting DurationDeserializer DateTimeException message to something similar as Instance
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like where this is going, but I think we should first add new unit tests proving the extant behavior in these edge cases (ignoring the performance problem). Then fix the performance problem. That makes it much easier to prove that we aren't changing the failure modes, just improving the performance.
@@ -40,6 +40,11 @@ | |||
|
|||
public static final DurationDeserializer INSTANCE = new DurationDeserializer(); | |||
|
|||
private static final BigDecimal DURATION_MAX = new BigDecimal( | |||
Long.MAX_VALUE + "." + java.time.Instant.MAX.getNano()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd write these bounds as (in effect) MAX+1 and MIN-1 which have the same safety-net effect to guard againstBigDecimal.longValue
while letting Duration
worry about the specific sub-second boundary.
(I'd rather not have to worry about, for example, how Duration
deals with fractions with more decimal places than nanoseconds.)
if(value.compareTo(DURATION_MAX) > 0 || | ||
value.compareTo(DURATION_MIN) < 0) { | ||
throw new DateTimeException( | ||
"Instant exceeds minimum or maximum Duration"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changes the behavior, and I agree with @yishaigalatzer that we shouldn't change behavior while patching this problem.
It's safer simply guard against the unbounded-execution time by catching the cases where the long
will be chopped to zero. I think that looks more or less like:
// Quickly handle zero || no low-order bits || < 1.0
if (value.signum() == 0 || value.scale() < -63 || value.precision() - value.scale() <= 0) {
seconds = 0;
} else {
seconds = value.longValue();
}
That preserves the existing (though undesirable) behavior for large values with random low-order bits, and doesn't throw exceptions in new cases. As @cowtowncoder implied, that change requires careful design consideration, and shouldn't block this fix.
@@ -17,6 +17,7 @@ | |||
package com.fasterxml.jackson.datatype.jsr310.deser; | |||
|
|||
import com.fasterxml.jackson.annotation.JsonFormat; | |||
import com.fasterxml.jackson.core.JsonParseException; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still needed? I think all three added imports are leftover from a prior revision.
@@ -53,28 +54,36 @@ public void testDeserializationAsFloat03() throws Exception | |||
assertEquals("The value is not correct.", Duration.ofSeconds(13498L, 8374), value); | |||
} | |||
|
|||
@Test | |||
public void testDeserializationAsFloat04() throws Exception |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks the same as the previous test case. Am I missing something?
I'm just submitted PR #85 adding a bunch of test cases around the boundary edges. I believe it's a superset of what's tested here. I suggest we get that reviewed and pulled and then this change can layered on top of it. The fix in that case wouldn't need any test changes, expect to add some low timeouts to the (8) currently-slow test cases. |
Ok, I merged @toddjonker's alternative, which I think truncates all out-of-bounds cases to zero value (that is, won't bother with upper-bound check for truncation or exception), which seems like Will close this PR but we can continue discussion either on #90 comments, new issue, mailing list or gitter. I think there are still remaining potential issues that could be tackled for 2.9.8. |
Is there already a CVE issued for this problem? When will 2.9.8 be released? |
@1Jesper1 I haven't requested CVE, but if someone has or is aware of one, I can add that. |
Guards against numbers causing CPU or OOM issues when deserializing
large numbers into Instant or Duration (see FasterXML/jackson-databind#2141
) by either: