Skip to content

Bound numeric string length before parsing - #154689

Merged
reugn merged 5 commits into
elastic:mainfrom
reugn:fix/bound-numeric-string-length
Jul 29, 2026
Merged

reugn merged 5 commits into
elastic:mainfrom
reugn:fix/bound-numeric-string-length

Conversation

@reugn

@reugn reugn commented Jul 22, 2026

Copy link
Copy Markdown
Member

Coercing a numeric value supplied as a string goes through new BigDecimal(String) or new BigInteger(String), whose parsing cost grows with the number of digits (super-linearly for the BigDecimal and BigInteger radix-10 constructors). The JSON parser bounds the length of an unquoted number token, but a number supplied as a quoted string is not bounded, so a single small request carrying a long digit string can consume a large amount of CPU on the thread that parses it.

This change bounds the length of a numeric string before it is parsed, using the same limit that already applies to an unquoted JSON number token. The check covers every path that coerces a user supplied numeric string to a number, including the accessors that parse via Double.parseDouble or Float.parseFloat and not only the BigDecimal/BigInteger sites, so the bound is uniform for all numeric types: the streaming parser used at index time (every integer and floating point accessor), the number and unsigned_long field mappers, and the ES|QL and SQL numeric literal and cast parsers. A value longer than the limit is rejected before the costly numeric parsing and conversion, and the limit is far above any real numeric value so valid input is unaffected.

The same string to number coercion is implemented independently in several layers, so the guard is added in each. The ES|QL and SQL/EQL libraries depend on server and reuse Numbers.MAX_NUMERIC_STRING_LENGTH directly. libs/x-content sits below server and cannot depend on it, so the same limit is duplicated in AbstractXContentParser, with matching values and a cross reference comment to keep the two in sync.

@reugn
reugn requested a review from a team as a code owner July 22, 2026 10:24
@reugn reugn added >bug auto-backport Automatically create backport pull requests when merged Team:Search Foundations Meta label for the Search Foundations team in Elasticsearch :Search Foundations/Search Catch all for Search Foundations v9.4.0 v9.5.0 v9.6.0 labels Jul 22, 2026
@elasticsearchmachine

Copy link
Copy Markdown
Collaborator

Pinging @elastic/es-search-foundations (Team:Search Foundations)

@elasticsearchmachine

Copy link
Copy Markdown
Collaborator

Hi @reugn, I've created a changelog YAML for you.

@github-actions

github-actions Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

🔍 Preview links for changed docs

⏳ Building and deploying preview... View progress

This comment will be updated with preview links when the build is complete.

@github-actions

Copy link
Copy Markdown
Contributor

ℹ️ Important: Docs version tagging

👋 Thanks for updating the docs! Just a friendly reminder that our docs are now cumulative. This means all 9.x versions are documented on the same page and published off of the main branch, instead of creating separate pages for each minor version.

We use applies_to tags to mark version-specific features and changes.

Expand for a quick overview

When to use applies_to tags:

✅ At the page level to indicate which products/deployments the content applies to (mandatory)
✅ When features change state (e.g. preview, ga) in a specific version
✅ When availability differs across deployments and environments

What NOT to do:

❌ Don't remove or replace information that applies to an older version
❌ Don't add new information that applies to a specific version without an applies_to tag
❌ Don't forget that applies_to tags can be used at the page, section, and inline level

🤔 Need help?

@rjernst rjernst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple thoughts

if (token == Token.VALUE_STRING) {
checkCoerceString(coerce, Short.class);

checkNumericStringLength(text());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optimizedText() would allow to check the length without materializing the string

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rjernst, applied.


// Numeric strings longer than this are rejected before coercion, whose cost grows with the digit count;
// matches the unquoted JSON number-token limit. Mirrored by AbstractXContentParser#MAX_NUMERIC_STRING_LENGTH. Keep in sync.
public static final int MAX_NUMERIC_STRING_LENGTH = 1000;

@rjernst rjernst Jul 22, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why we need support for nearly this many digits. Elasticsearch only supports indexing up to unsigned longs, not arbitrary length numbers.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The limit matches the unquoted JSON number-token limit (Jackson's maxNumberLength default of 1000). Parsing a 1000-digit number is trivially fast.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parsing 1000 digits implies that ES will actually use it, but it won't. I don't think we need to match Jackson's default.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Max unsigned long requires 20 chars, double in scientific notation needs 24. But plain double literals can take hundreds of digits, while still being valid, so 1000 gives safe headroom.

public void testNumericCoercionRejectsOversizedString() throws IOException {
// Every numeric accessor bounds the length of a quoted string before coercing it, avoiding unbounded
// (and, for long, super-linear) parsing work on an attacker-sized value.
String oversized = "1" + "0".repeat(5000);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to structure this test to ensure that strings at MAX_NUMERIC_STRING_LENGTH parse but MAX_NUMERIC_STRING_LENGTH+1 throw an exception like the other tests below.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required making MAX_NUMERIC_STRING_LENGTH public, but agreed, it's better. Refactored the test.

@chrisparrinello chrisparrinello left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks for taking care of this!

@reugn
reugn merged commit f407f9a into elastic:main Jul 29, 2026
43 checks passed
@elasticsearchmachine

Copy link
Copy Markdown
Collaborator

💔 Backport failed

Status Branch Result
❌ 9.4 Commit could not be cherrypicked due to conflicts
✅ 9.5

You can use sqren/backport to manually backport by running backport --upstream elastic/elasticsearch --pr 154689

@reugn
reugn deleted the fix/bound-numeric-string-length branch July 29, 2026 08:21
elasticsearchmachine pushed a commit that referenced this pull request Jul 29, 2026
Coercing a numeric value supplied as a string goes through new BigDecimal(String) or new BigInteger(String), whose parsing cost grows with the number of digits (super-linearly for the BigDecimal and BigInteger radix-10 constructors). The JSON parser bounds the length of an unquoted number token, but a number supplied as a quoted string is not bounded, so a single small request carrying a long digit string can consume a large amount of CPU on the thread that parses it.

This change bounds the length of a numeric string before it is parsed, using the same limit that already applies to an unquoted JSON number token. The check covers every path that coerces a user supplied numeric string to a number, including the accessors that parse via Double.parseDouble or Float.parseFloat and not only the BigDecimal/BigInteger sites, so the bound is uniform for all numeric types: the streaming parser used at index time (every integer and floating point accessor), the number and unsigned_long field mappers, and the ES|QL and SQL numeric literal and cast parsers. A value longer than the limit is rejected before the costly numeric parsing and conversion, and the limit is far above any real numeric value so valid input is unaffected.
elasticsearchmachine pushed a commit that referenced this pull request Jul 29, 2026
Coercing a numeric value supplied as a string goes through new BigDecimal(String) or new BigInteger(String), whose parsing cost grows with the number of digits (super-linearly for the BigDecimal and BigInteger radix-10 constructors). The JSON parser bounds the length of an unquoted number token, but a number supplied as a quoted string is not bounded, so a single small request carrying a long digit string can consume a large amount of CPU on the thread that parses it.

This change bounds the length of a numeric string before it is parsed, using the same limit that already applies to an unquoted JSON number token. The check covers every path that coerces a user supplied numeric string to a number, including the accessors that parse via Double.parseDouble or Float.parseFloat and not only the BigDecimal/BigInteger sites, so the bound is uniform for all numeric types: the streaming parser used at index time (every integer and floating point accessor), the number and unsigned_long field mappers, and the ES|QL and SQL numeric literal and cast parsers. A value longer than the limit is rejected before the costly numeric parsing and conversion, and the limit is far above any real numeric value so valid input is unaffected.

(cherry picked from commit f407f9a)
elasticsearchmachine pushed a commit that referenced this pull request Jul 29, 2026
Coercing a numeric value supplied as a string goes through new BigDecimal(String) or new BigInteger(String), whose parsing cost grows with the number of digits (super-linearly for the BigDecimal and BigInteger radix-10 constructors). The JSON parser bounds the length of an unquoted number token, but a number supplied as a quoted string is not bounded, so a single small request carrying a long digit string can consume a large amount of CPU on the thread that parses it.

This change bounds the length of a numeric string before it is parsed, using the same limit that already applies to an unquoted JSON number token. The check covers every path that coerces a user supplied numeric string to a number, including the accessors that parse via Double.parseDouble or Float.parseFloat and not only the BigDecimal/BigInteger sites, so the bound is uniform for all numeric types: the streaming parser used at index time (every integer and floating point accessor), the number and unsigned_long field mappers, and the ES|QL and SQL numeric literal and cast parsers. A value longer than the limit is rejected before the costly numeric parsing and conversion, and the limit is far above any real numeric value so valid input is unaffected.

(cherry picked from commit f407f9a)
elasticsearchmachine pushed a commit that referenced this pull request Jul 29, 2026
Coercing a numeric value supplied as a string goes through new BigDecimal(String) or new BigInteger(String), whose parsing cost grows with the number of digits (super-linearly for the BigDecimal and BigInteger radix-10 constructors). The JSON parser bounds the length of an unquoted number token, but a number supplied as a quoted string is not bounded, so a single small request carrying a long digit string can consume a large amount of CPU on the thread that parses it.

This change bounds the length of a numeric string before it is parsed, using the same limit that already applies to an unquoted JSON number token. The check covers every path that coerces a user supplied numeric string to a number, including the accessors that parse via Double.parseDouble or Float.parseFloat and not only the BigDecimal/BigInteger sites, so the bound is uniform for all numeric types: the streaming parser used at index time (every integer and floating point accessor), the number and unsigned_long field mappers, and the ES|QL and SQL numeric literal and cast parsers. A value longer than the limit is rejected before the costly numeric parsing and conversion, and the limit is far above any real numeric value so valid input is unaffected.

(cherry picked from commit f407f9a)
jan-elastic pushed a commit to jan-elastic/elasticsearch that referenced this pull request Jul 29, 2026
Coercing a numeric value supplied as a string goes through new BigDecimal(String) or new BigInteger(String), whose parsing cost grows with the number of digits (super-linearly for the BigDecimal and BigInteger radix-10 constructors). The JSON parser bounds the length of an unquoted number token, but a number supplied as a quoted string is not bounded, so a single small request carrying a long digit string can consume a large amount of CPU on the thread that parses it.

This change bounds the length of a numeric string before it is parsed, using the same limit that already applies to an unquoted JSON number token. The check covers every path that coerces a user supplied numeric string to a number, including the accessors that parse via Double.parseDouble or Float.parseFloat and not only the BigDecimal/BigInteger sites, so the bound is uniform for all numeric types: the streaming parser used at index time (every integer and floating point accessor), the number and unsigned_long field mappers, and the ES|QL and SQL numeric literal and cast parsers. A value longer than the limit is rejected before the costly numeric parsing and conversion, and the limit is far above any real numeric value so valid input is unaffected.
tballison pushed a commit to tballison/elasticsearch that referenced this pull request Aug 4, 2026
Coercing a numeric value supplied as a string goes through new BigDecimal(String) or new BigInteger(String), whose parsing cost grows with the number of digits (super-linearly for the BigDecimal and BigInteger radix-10 constructors). The JSON parser bounds the length of an unquoted number token, but a number supplied as a quoted string is not bounded, so a single small request carrying a long digit string can consume a large amount of CPU on the thread that parses it.

This change bounds the length of a numeric string before it is parsed, using the same limit that already applies to an unquoted JSON number token. The check covers every path that coerces a user supplied numeric string to a number, including the accessors that parse via Double.parseDouble or Float.parseFloat and not only the BigDecimal/BigInteger sites, so the bound is uniform for all numeric types: the streaming parser used at index time (every integer and floating point accessor), the number and unsigned_long field mappers, and the ES|QL and SQL numeric literal and cast parsers. A value longer than the limit is rejected before the costly numeric parsing and conversion, and the limit is far above any real numeric value so valid input is unaffected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-backport Automatically create backport pull requests when merged >bug :Search Foundations/Search Catch all for Search Foundations Team:Search Foundations Meta label for the Search Foundations team in Elasticsearch v9.4.0 v9.5.0 v9.6.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants