Im using Spring Boot 3.3.3. When I try to use Spring properties that contain curly braces in logback-spring.xml they are not escaped.

I have the following application.yml:

smtp:
  host: myhost
  username: myusername
  password: "A{B" # note the curly brace

logback-spring.xml for the Logback extension:

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

    <springProperty name="smtpHost" source="smtp.host" defaultValue="not-set"/>
    <springProperty name="smtpUsername" source="smtp.username" defaultValue="not-set"/>
    <springProperty name="smtpPassword" source="smtp.password" defaultValue="not-set"/>

    <appender name="EMAIL" class="ch.qos.logback.classic.net.SMTPAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <smtpHost>${smtpHost}</smtpHost>
        <username>${smtpUsername}</username>
        <password>${smtpPassword}</password>
        ...
    </appender>

    <springProfile name="dev">
        <root level="INFO">
            <appender-ref ref="CONSOLE"/>
            <appender-ref ref="EMAIL"/>
        </root>
    </springProfile>

</configuration>

When I start my application I get an exception as the smtpPassword is not properly substituted. The problem disappears if I remove the curly brace from the password. Unfortunately, in production that is not an option for me. I'm also not able to escape the password e.g. with A'{'B or A\\{B. Here is the exception I'm getting:

Logging system failed to initialize using configuration from 'null'
java.lang.IllegalStateException: Logback configuration error detected: 
ERROR in ch.qos.logback.core.model.util.VariableSubstitutionsHelper@3e37aa90 - Problem while parsing [${smtpPassword}] java.lang.IllegalArgumentException: All tokens consumed but was expecting "}"
    at org.springframework.boot.logging.logback.LogbackLoggingSystem.reportConfigurationErrorsIfNecessary(LogbackLoggingSystem.java:282)
    at org.springframework.boot.logging.logback.LogbackLoggingSystem.loadConfiguration(LogbackLoggingSystem.java:260)
    at org.springframework.boot.logging.AbstractLoggingSystem.initializeWithConventions(AbstractLoggingSystem.java:81)
    at org.springframework.boot.logging.AbstractLoggingSystem.initialize(AbstractLoggingSystem.java:61)
    at org.springframework.boot.logging.logback.LogbackLoggingSystem.initialize(LogbackLoggingSystem.java:193)
    at org.springframework.boot.context.logging.LoggingApplicationListener.initializeSystem(LoggingApplicationListener.java:332)
    at org.springframework.boot.context.logging.LoggingApplicationListener.initialize(LoggingApplicationListener.java:298)
    at org.springframework.boot.context.logging.LoggingApplicationListener.onApplicationEnvironmentPreparedEvent(LoggingApplicationListener.java:246)
    at org.springframework.boot.context.logging.LoggingApplicationListener.onApplicationEvent(LoggingApplicationListener.java:223)
    at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185)
    at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178)
    at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156)
    at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:138)
    at org.springframework.boot.context.event.EventPublishingRunListener.multicastInitialEvent(EventPublishingRunListener.java:136)
    at org.springframework.boot.context.event.EventPublishingRunListener.environmentPrepared(EventPublishingRunListener.java:81)
    at org.springframework.boot.SpringApplicationRunListeners.lambda$environmentPrepared$2(SpringApplicationRunListeners.java:64)
    at java.base/java.lang.Iterable.forEach(Iterable.java:75)
    at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:118)
    at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:112)
    at org.springframework.boot.SpringApplicationRunListeners.environmentPrepared(SpringApplicationRunListeners.java:63)
    at org.springframework.boot.SpringApplication.prepareEnvironment(SpringApplication.java:370)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:330)
    at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:149)
    at org.MyApp.main(MyApp.java:28)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:50)
    Suppressed: java.lang.IllegalArgumentException: All tokens consumed but was expecting "}"
        at ch.qos.logback.core.subst.Parser.expectNotNull(Parser.java:160)
        at ch.qos.logback.core.subst.Parser.expectCurlyRight(Parser.java:165)
        at ch.qos.logback.core.subst.Parser.T(Parser.java:93)
        at ch.qos.logback.core.subst.Parser.E(Parser.java:58)
        at ch.qos.logback.core.subst.Parser.Eopt(Parser.java:75)
        at ch.qos.logback.core.subst.Parser.E(Parser.java:62)
        at ch.qos.logback.core.subst.Parser.Eopt(Parser.java:75)
        at ch.qos.logback.core.subst.Parser.E(Parser.java:62)
        at ch.qos.logback.core.subst.Parser.Eopt(Parser.java:75)
        at ch.qos.logback.core.subst.Parser.E(Parser.java:62)
        at ch.qos.logback.core.subst.Parser.Eopt(Parser.java:75)
        at ch.qos.logback.core.subst.Parser.E(Parser.java:62)
        at ch.qos.logback.core.subst.Parser.Eopt(Parser.java:75)
        at ch.qos.logback.core.subst.Parser.E(Parser.java:62)
        at ch.qos.logback.core.subst.Parser.parse(Parser.java:54)
        at ch.qos.logback.core.subst.NodeToStringTransformer.tokenizeAndParseString(NodeToStringTransformer.java:59)
        at ch.qos.logback.core.subst.NodeToStringTransformer.handleVariable(NodeToStringTransformer.java:102)
        at ch.qos.logback.core.subst.NodeToStringTransformer.compileNode(NodeToStringTransformer.java:77)
        at ch.qos.logback.core.subst.NodeToStringTransformer.transform(NodeToStringTransformer.java:64)
        at ch.qos.logback.core.subst.NodeToStringTransformer.substituteVariable(NodeToStringTransformer.java:52)
        at ch.qos.logback.core.util.OptionHelper.substVars(OptionHelper.java:112)
        at ch.qos.logback.core.model.util.VariableSubstitutionsHelper.subst(VariableSubstitutionsHelper.java:52)
        at ch.qos.logback.core.model.processor.ModelInterpretationContext.subst(ModelInterpretationContext.java:166)
        at ch.qos.logback.core.model.processor.ImplicitModelHandler.doBasicProperty(ImplicitModelHandler.java:105)
        at ch.qos.logback.core.model.processor.ImplicitModelHandler.handle(ImplicitModelHandler.java:86)
        at ch.qos.logback.core.model.processor.DefaultProcessor.secondPhaseTraverse(DefaultProcessor.java:241)
        at ch.qos.logback.core.model.processor.DefaultProcessor.secondPhaseTraverse(DefaultProcessor.java:253)
        at ch.qos.logback.core.model.processor.DefaultProcessor.secondPhaseTraverse(DefaultProcessor.java:253)
        at ch.qos.logback.core.model.processor.DefaultProcessor.traversalLoop(DefaultProcessor.java:90)
        at ch.qos.logback.core.model.processor.DefaultProcessor.process(DefaultProcessor.java:106)
        at ch.qos.logback.core.joran.GenericXMLConfigurator.processModel(GenericXMLConfigurator.java:216)
        at org.springframework.boot.logging.logback.SpringBootJoranConfigurator.processModel(SpringBootJoranConfigurator.java:133)
        at ch.qos.logback.core.joran.GenericXMLConfigurator.doConfigure(GenericXMLConfigurator.java:178)
        at ch.qos.logback.core.joran.GenericXMLConfigurator.doConfigure(GenericXMLConfigurator.java:123)
        at ch.qos.logback.core.joran.GenericXMLConfigurator.doConfigure(GenericXMLConfigurator.java:66)
        at org.springframework.boot.logging.logback.LogbackLoggingSystem.configureByResourceUrl(LogbackLoggingSystem.java:292)
        at org.springframework.boot.logging.logback.LogbackLoggingSystem.lambda$loadConfiguration$1(LogbackLoggingSystem.java:254)
        at org.springframework.boot.logging.logback.LogbackLoggingSystem.withLoggingSuppressed(LogbackLoggingSystem.java:472)
        at org.springframework.boot.logging.logback.LogbackLoggingSystem.loadConfiguration(LogbackLoggingSystem.java:248)
        ... 25 more

Comment From: philwebb

Unfortunately this looks a Logback bug and we don't have the option of escaping values. See https://jira.qos.ch/browse/LOGBACK-1461 and https://github.com/qos-ch/logback/issues/836. If either of those issues are fixed we can reopen this one and automatically apply escaping.