Skip to content

Introduce support for escaping the escape character for property placeholders #34315

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

Open
bitb4ker opened this issue Jan 24, 2025 · 15 comments
Open
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Milestone

Comments

@bitb4ker
Copy link

bitb4ker commented Jan 24, 2025

Up until spring-boot 3.3.8, an escaped backslash followed by a placeholder like:

prop1=value1  
prop2=value2\\${prop1}  

Resulted in prop2 resolving to:

value2\value1

Starting with spring-boot 3.4.0, the result is:

value2value1

Doubling the backslashes like this:

prop1=value1  
prop2=value2\\\\${prop1}  

Results in:

value2${prop1}

This is due to the escaping logic introduced in org.springframework.util.PlaceholderParser which does two passes of PlaceholderParser.SimplePlaceholderPart.resolveRecursively(PartResolutionContext, String), each calling PlaceholderParser.parse(String, boolean) in which the escaping logic is implemented.

This last method does not handle escaping the escape character, causing the issue.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jan 24, 2025
@bclozel bclozel transferred this issue from spring-projects/spring-boot Jan 24, 2025
@snicoll
Copy link
Member

snicoll commented Jan 24, 2025

The parser in 6.2 has been rewritten to fix, amongst other things, #9628. Unfortunately, this means that if the placeholder is escaped, it's rendered as is (i.e. not evaluated).

I wrote a test:

@Test
void gh34315() {
    Properties properties = new Properties();
    properties.setProperty("prop1", "value1");
    properties.setProperty("prop2", "value2\\${prop1}");
    PlaceholderParser parser = new PlaceholderParser("${", "}", ":", '\\', true);
    assertThat(parser.replacePlaceholders("${prop2}", properties::getProperty)).isEqualTo("value2${prop1}");
}

and it passes. I've also added those two properties in an empty Spring 6.2 app and it resolved the same way. Can you share a sample that actually fails the way you've described?

@snicoll snicoll added the status: waiting-for-feedback We need additional information before we can continue label Jan 24, 2025
@bitb4ker
Copy link
Author

bitb4ker commented Jan 24, 2025

props-spring-3.3.zip

props-spring-3.4.zip

To run:

gradlew bootRun

The 3.3 one shows the expected behavior, the 3.4 shows what I described as the broken behavior in both '\' and '\\' in the original post.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jan 24, 2025
@snicoll
Copy link
Member

snicoll commented Jan 26, 2025

I got it now. The problem is PropertySourcesPlaceholderConfigurer getting in the way of the placeholder resolution. It does its own round, removes the backslash to render the placeholder as is. As a result, the parser continues its work and sees a new placeholder to resolve.

@snicoll
Copy link
Member

snicoll commented Jan 27, 2025

The issue as reported can't be fixed without re-introducing the bug that we fixed. The side effect that was found as part of the report is going to be handled by #34326.

For that case above, you'll need to restructure your configuration to move the backslash elsewhere to avoid the escaping, something like:

prop1=\\value1
prop2=value2${prop1}

@snicoll snicoll closed this as not planned Won't fix, can't repro, duplicate, stale Jan 27, 2025
@snicoll snicoll added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-triage An issue we've not yet triaged or decided on status: feedback-provided Feedback has been provided labels Jan 27, 2025
@bitb4ker
Copy link
Author

bitb4ker commented Jan 27, 2025

Couldn't we introduce a way to escape the escape character?
This is essentially the root of the issue I raised.
Why does it break previous fixes?

@snicoll
Copy link
Member

snicoll commented Jan 28, 2025

Everything is possible, I guess but we’re not keen to make the parser more complex at this time.

@bitb4ker
Copy link
Author

My issue is that this broke something that was working for years.
We have dozen of config files now broken because we build windows path and windows logins with placeholders.

@neoludo
Copy link

neoludo commented Mar 14, 2025

@bitb4ker is right, that's a breaking change. You should at least mention it in the release notes.

@bclozel
Copy link
Member

bclozel commented Mar 14, 2025

@neoludo Can you suggest what's missing from the release notes?

@neoludo
Copy link

neoludo commented Mar 14, 2025

The sentence If you used the escaped character right before the placeholder, you will need to modify your configuration structure to move \\ in the value itself. seems to be correct for properties files only.

But in a YAML file, and given this YAML snippet :

username: johndoe
login: DOMAIN\${username}

login will be evaluated as : DOMAINjohndoe

and given this YAML snippet :

username: johndoe  
login: DOMAIN\\${username}

login will be evaluated correctly as : DOMAIN\johndoe

So there is no need to move backslashes to value itself, and the upgrade notes should dissociate YAML parsing from properties parsing.

@bclozel
Copy link
Member

bclozel commented Mar 14, 2025

I guess this is a Yaml specific concern; but I guess the advice to move the \ directly into one of the values still applies.

It could be that this currently works because of some subtle YAML+parser interaction right now, but is still not advised.

@bitb4ker
Copy link
Author

@bclozel I see a paragraph was added on that in the release notes however I still think that this is an issue that needs to be addressed as being forced to add the \ in the value being interpolated is just not right.
In my case I deal with downlevel user accounts (DOMAIN\USERNAME) and paths and in neither case is adding the \ in the value being interpolated appropriate.

Could we add implement an escape mechanism for the escape character somewhere in the roadmap?

@sbrannen sbrannen added the in: core Issues in core modules (aop, beans, core, context, expression) label May 5, 2025
@sbrannen sbrannen self-assigned this May 7, 2025
@sbrannen sbrannen added type: enhancement A general enhancement and removed status: declined A suggestion or change that we don't feel we should currently apply labels May 7, 2025
@sbrannen sbrannen changed the title Escaping logic in PlaceholderParser does not handle escaped backslashes Introduce support for escaping the escape character for property placeholders May 7, 2025
@sbrannen
Copy link
Member

sbrannen commented May 7, 2025

Hi everyone,

Thanks for all of the constructive feedback! 👍

We have decided to introduce support for "escaping the escape character" in Spring Framework 7.0.

In light of that, I have changed the title of this issue and reopened this issue to address that.

In addition, we will be addressing #34861 and related issues in Spring Framework 6.2.7.

Cheers,

Sam

@bitb4ker
Copy link
Author

bitb4ker commented May 7, 2025

That's a great news, thanks!
I can't wait to test it!

@sbrannen
Copy link
Member

sbrannen commented May 7, 2025

In the interim, once #34861 is complete you should be able to explicitly set the escapeCharacter to null via custom configuration.

Whereas, once #34865 is complete you should be able to globally disable the escape character by default, via a JVM system property, directly via SpringProperties, or within your project in a spring.properties file.

Thus, 6.2.7 will provide mechanisms that can be used as an alternative to "escaping the escape character" within configuration strings.

sbrannen added a commit to sbrannen/spring-framework that referenced this issue May 10, 2025
Currently, the placeholder resolution algorithm in
PropertySourcesPlaceholderConfigurer fails in several scenarios, and
the root cause for this category of failures has actually existed since
PropertySourcesPlaceholderConfigurer was introduced in Spring Framework
3.1.

Specifically, PropertySourcesPlaceholderConfigurer creates its own
PropertySourcesPropertyResolver that indirectly delegates to another
"nested" PropertySourcesPropertyResolver to interact with
PropertySources from the Environment, which results in double
placeholder parsing and resolution attempts, and that behavior leads to
a whole category of bugs.

For example, spring-projects#27947 was addressed in Spring Framework 5.3.16, and due
to spring-projects#34315 and spring-projects#34326 we have recently realized that additional bugs
exist with placeholder resolution: nested placeholder resolution can
fail when escape characters are used, and it is currently impossible
to disable the escape character support for nested resolution.

To address this category of bugs, we no longer indirectly use or
directly create a "nested" PropertySourcesPropertyResolver in
PropertySourcesPlaceholderConfigurer. Instead, properties from property
sources from the Environment are now accessed directly without
duplicate/nested placeholder resolution.

See spring-projectsgh-27947
See spring-projectsgh-34326
See spring-projectsgh-34862
Closes spring-projectsgh-34861
sbrannen added a commit that referenced this issue May 10, 2025
Currently, the placeholder resolution algorithm in
PropertySourcesPlaceholderConfigurer fails in several scenarios, and
the root cause for this category of failures has actually existed since
PropertySourcesPlaceholderConfigurer was introduced in Spring Framework
3.1.

Specifically, PropertySourcesPlaceholderConfigurer creates its own
PropertySourcesPropertyResolver that indirectly delegates to another
"nested" PropertySourcesPropertyResolver to interact with
PropertySources from the Environment, which results in double
placeholder parsing and resolution attempts, and that behavior leads to
a whole category of bugs.

For example, #27947 was addressed in Spring Framework 5.3.16, and due
to #34315 and #34326 we have recently realized that additional bugs
exist with placeholder resolution: nested placeholder resolution can
fail when escape characters are used, and it is currently impossible
to disable the escape character support for nested resolution.

To address this category of bugs, we no longer indirectly use or
directly create a "nested" PropertySourcesPropertyResolver in
PropertySourcesPlaceholderConfigurer. Instead, properties from property
sources from the Environment are now accessed directly without
duplicate/nested placeholder resolution.

See gh-27947
See gh-34326
See gh-34862
Closes gh-34861
sbrannen added a commit to sbrannen/spring-framework that referenced this issue May 13, 2025
Spring Framework 6.2 introduced support for an escape character for
property placeholders (by default '\'). However, as of Spring Framework
6.2.6, there was no way to either escape the escape character or disable
escape character support.

For example, given a `username` property configured with the value of
`Jane.Smith` and a `DOMAIN\${username}` configuration string, property
placeholder replacement used to result in `DOMAIN\Jane.Smith` prior to
6.2 but now results in `DOMAIN${username}`. Similarly, an attempt to
escape the escape character via `DOMAIN\\${username}` results in
`DOMAIN\${username}`.

In theory, one should be able to disable use of an escape character
altogether, and that is currently possible by invoking
setEscapeCharacter(null) on AbstractPropertyResolver and
PlaceholderConfigurerSupport (the superclass of
PropertySourcesPlaceholderConfigurer).

However, in reality, there are two hurdles.

- As of 6.2.6, an invocation of setEscapeCharacter(null) on a
  PropertySourcesPlaceholderConfigurer applied to its internal
  top-level PropertySourcesPropertyResolver but not to any nested
  PropertySourcesPropertyResolver, which means that the `null` escape
  character could not be effectively applied.

- Users may not have an easy way to explicitly set the escape character
  to `null` for a PropertyResolver or
  PropertySourcesPlaceholderConfigurer. For example, Spring Boot
  auto-configures a PropertySourcesPlaceholderConfigurer with the
  default escape character enabled.

This first issue above has recently been addressed by spring-projectsgh-34861.

This commit therefore addresses the second issue as follows.

- To allow developers to easily revert to the pre-6.2 behavior without
  changes to code or configuration strings, this commit introduces a
  `spring.placeholder.escapeCharacter.default` property for use with
  SpringProperties which globally sets the default escape character that
  is automatically configured in AbstractPropertyResolver and
  PlaceholderConfigurerSupport.

- Setting the property to an empty string sets the default escape
  character to `null`, effectively disabling the default support for
  escape characters.

    spring.placeholder.escapeCharacter.default =

- Setting the property to any other character sets the default escape
  character to that specific character.

    spring.placeholder.escapeCharacter.default = ~

- Setting the property to a string containing more than one character
  results in an exception.

- Developers are still able to configure an explicit escape character
  in AbstractPropertyResolver and PlaceholderConfigurerSupport if they
  choose to do so.

- Third-party components that wish to rely on the same feature can
  invoke AbstractPropertyResolver.getDefaultEscapeCharacter() to obtain
  the globally configured default escape character.

See spring-projectsgh-9628
See spring-projectsgh-34315
See spring-projectsgh-34861
Closes spring-projectsgh-34865
sbrannen added a commit that referenced this issue May 13, 2025
Spring Framework 6.2 introduced support for an escape character for
property placeholders (by default '\'). However, as of Spring Framework
6.2.6, there was no way to either escape the escape character or disable
escape character support.

For example, given a `username` property configured with the value of
`Jane.Smith` and a `DOMAIN\${username}` configuration string, property
placeholder replacement used to result in `DOMAIN\Jane.Smith` prior to
6.2 but now results in `DOMAIN${username}`. Similarly, an attempt to
escape the escape character via `DOMAIN\\${username}` results in
`DOMAIN\${username}`.

In theory, one should be able to disable use of an escape character
altogether, and that is currently possible by invoking
setEscapeCharacter(null) on AbstractPropertyResolver and
PlaceholderConfigurerSupport (the superclass of
PropertySourcesPlaceholderConfigurer).

However, in reality, there are two hurdles.

- As of 6.2.6, an invocation of setEscapeCharacter(null) on a
  PropertySourcesPlaceholderConfigurer applied to its internal
  top-level PropertySourcesPropertyResolver but not to any nested
  PropertySourcesPropertyResolver, which means that the `null` escape
  character could not be effectively applied.

- Users may not have an easy way to explicitly set the escape character
  to `null` for a PropertyResolver or
  PropertySourcesPlaceholderConfigurer. For example, Spring Boot
  auto-configures a PropertySourcesPlaceholderConfigurer with the
  default escape character enabled.

This first issue above has recently been addressed by gh-34861.

This commit therefore addresses the second issue as follows.

- To allow developers to easily revert to the pre-6.2 behavior without
  changes to code or configuration strings, this commit introduces a
  `spring.placeholder.escapeCharacter.default` property for use with
  SpringProperties which globally sets the default escape character that
  is automatically configured in AbstractPropertyResolver and
  PlaceholderConfigurerSupport.

- Setting the property to an empty string sets the default escape
  character to `null`, effectively disabling the default support for
  escape characters.

    spring.placeholder.escapeCharacter.default =

- Setting the property to any other character sets the default escape
  character to that specific character.

    spring.placeholder.escapeCharacter.default = ~

- Setting the property to a string containing more than one character
  results in an exception.

- Developers are still able to configure an explicit escape character
  in AbstractPropertyResolver and PlaceholderConfigurerSupport if they
  choose to do so.

- Third-party components that wish to rely on the same feature can
  invoke AbstractPropertyResolver.getDefaultEscapeCharacter() to obtain
  the globally configured default escape character.

See gh-9628
See gh-34315
See gh-34861
Closes gh-34865
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

6 participants