Skip to content
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

Release 3.0.12 has broken most of our templates due to #809 #816

Closed
scoldwell opened this issue Jan 8, 2021 · 23 comments
Closed

Release 3.0.12 has broken most of our templates due to #809 #816

scoldwell opened this issue Jan 8, 2021 · 23 comments
Assignees

Comments

@scoldwell
Copy link

scoldwell commented Jan 8, 2021

Due to changes in #809 we have a lot of templates that are now broken.

For a lot of our templates we use static members in our Spring controllers for the request names. Hrefs within our templates look like the following:

<a th:href="@{${T(com.test.controller.DashboardController).REQUEST_DASHBOARD}}">

This now results in the following exception:

org.thymeleaf.exceptions.TemplateProcessingException: Instantiation of new objects and access to static classes is forbidden in this context

This pattern is used in 100's of templates in our code base. We obviously don't want to completely disable the restricted expression evaluation mode, but is there a way to turn off the restriction of just the static class access?

Thanks,
Scott

@abs0
Copy link

abs0 commented Jan 15, 2021

Very much likewise - there seem to be a lot of thymeleaf examples using just this pattern and we've used it across our system.

I'd even be happy with a new syntax for accessing enums (regex being your (sometimes sharp edged) friend :)

@abollaert
Copy link

abollaert commented Jan 18, 2021

It looks like this gets applied selectively though (for example a th:each works whereas a th:data-xxx doesn't). The only problematic instances I found in our code were "default" attributes (such as th:data-whatever or th:aria-labelledby).

@leonard84
Copy link

As the intent of the restricted expression evaluation mode is to:

reduce the risks of code injection as much as possible. Thymeleaf does this by preventing the use of direct input from users in certain parts of the template. This direct input from users refers to request parameters, as these might not have passed a validation process at the controller.
(source)

IMHO restricting access to constants and enums doesn't make sense to achieve this goal, as they cannot be manipulated by user input.

@danielfernandez as you are responsible for #809 I would like to know what the suggested replacement for accessing constants is?

Would it be possible for Thymeleaf to determine that expressions reference constants and allow those?.

@membersound
Copy link

membersound commented Feb 9, 2021

Here too. Most of our template broke with 3.0.12, which is part of spring-boot-starter-thymeleaf-2.4.2.
Thus atm we have to downgrade thymeleaf explicit...

Martin-BG added a commit to Martin-BG/Marketplace that referenced this issue Feb 14, 2021
- Spring Boot 2.4.0 -> 2.4.2
- Lombok 1.18.16 -> 1.18.18
- JUnit 5.7.0 -> 4.7.1
- JUnit 4.13.1 -> 4.13.2
- Mockito 3.6.28 -> 3.7.7
- Spring Security: 5.4.2 -> 5.4.4
- Hibernate: 5.4.25.Final -> 5.4.38.Final
- Hibernate Validator: 6.1.6.Final -> 6.2.0.Final
- Bootstrap: 4.5.3 -> 4.6.0
- PopperJS: 2.5.2 -> 2.5.4

Notes:
- Thymeleaf version is explicitly downgraded to 3.0.11.RELEASE as there's issue in 3.0.12.RELEASE:
    thymeleaf/thymeleaf#816
- Application doesn't start with Hibernate Validator 7.0.1.Final - further investigation is needed
ginccc added a commit to labsai/EDDI that referenced this issue Feb 15, 2021
@sunriize
Copy link

sunriize commented Mar 2, 2021

+1 Many of my templates are broken due to this "minor"?? update. It looks like a breaking change to me and should not have been embedded within a minor version update...

@venustulips
Copy link

I really wonder why there is no mention of the alternatives.
I ended up using with th:with like below

th:with="rptBy=${T(com.test.enum.ReportBy).BY_G1}"
th:data-groupby="${rptBy}"

@scoldwell
Copy link
Author

@danielfernandez can you comment on this issue? Seems to be a major problem for many people

@jlSites
Copy link

jlSites commented Mar 26, 2021

I had the same issue on my spring boot project(2.4.4), maybe 30+ templates affected, so spend few hours refactoring them into model attributes.

@membersound
Copy link

I had the same issue on my spring boot project(2.4.4), maybe 30+ templates affected, so spend few hours refactoring them into model attributes.

Could you maybe give examples on which expressions have to be formatted in a new way? That would probably help the majority of us to get rid of the old syntax more easily.

@venustulips
Copy link

If you are working with Spring then another workaround to this is to define global model attributes using @ControllerAdvice.
For eg,

@ControllerAdvice
public class GlobalModelAttribute {

    @ModelAttribute("myEnum1List")
    public String myEnum1List() {
        return enumList;
    }

    @ModelAttribute("constantList")
    public String constantList() {
        return constantList;
    }

    @ModelAttribute("aConstant")
    public String aConstant() {
        return aConstant;
    }
}

rferranti added a commit to optionfactory/optionfactory-spring that referenced this issue Apr 25, 2021
@danielfernandez
Copy link
Member

danielfernandez commented Apr 25, 2021

Please note that, as explained in #809, execution of static code with T(className) has only been forbidden in specific restricted scenarios. Namely:

  • Pre-processing expressions: __...__
  • Unescaped output: th:utext and inlined unescaped expressions.
  • All th:on* attribute processors for JavaScript events which value is a Thymeleaf Standard Expression (see Enable processing of HTML event handler attributes in JAVASCRIPT template mode #707).
  • th:attr attribute processor that enables the creation of variables with arbitrary name to be used elsewhere in the template.
  • Template and fragment names in Fragment expressions: ~{...} or contents of th:insert, th:replace, th:include and th:substituteby
  • Fragment parameters in Fragment Expressions (~{...} or th:insert, th:replace, etc.) so that template fragments cannot be called with parameters which values come from direct user input (would be a scenario equivalent to that of th:attr).
  • Default attribute processor (see Default attribute support #297), which allows the rendering of any attribute with an arbitrary name (just in case it is a JS event or anything similar)
  • URL bases in Link Expressions (@{...}) (URL parameters will not be restricted)
  • th:src and th:href (except URL parameters inside Link Expressions as explained above)
  • Output expressions in TEXT template mode, even if escaped, any use in any position (safest due to the lack of knowledge on the use it's being given).

This indeed includes expressions like the one in the original post in this ticket:

<a th:href="@{${T(com.test.controller.DashboardController).REQUEST_DASHBOARD}}">

This restriction was established for security reasons, and as part of the result of a vulnerability report. Link expressions are especially sensitive from a security standpoint, and both the creation of new objects (new ...) and execution of static code (T(...)) become problematic here in two ways:

  • First, because these expressions allow the execution of arbitrary code from anywhere in the class path, including classes which might come from libraries on which the developers of the web application might have little or no control.
  • Second, that there are many scenarios in which user input (e.g. request parameters or values from data storage) might be added at the controller as model attributes without validation and then used as part of url links in templates. This user input could end up creating links for URLs that might contain fragments asking for the execution of this arbitrary code explained above, or even worst things like e.g. calls to T(java.lang.Runtime).getRuntime().exec(...). This, combined with a request resolver at the other side that might automatically execute expressions in paths –which is not common, but possible– would open a window for remote code execution.

Workaround

There are several alternatives for T(...) code in links (or other restricted scenarios), but all of them need to go the way of assuring Thymeleaf that the code to be executed is under the control of the developer.

Using th:with attributes is a possibility, because at least that way Thymeleaf is sure that developers really want to do that (and hopefully they know what they are doing). But I would rather recommend moving that code to objects added to the model as attributes – if you are interested in using a series of constants with paths, then make those accessible with getters.

So my recommendation would be to follow @venustulips example above and define @ModelAttribute objects containing the data you want to access from your templates, either directly at your @Controller classes or at @ControllerAdvice ones.

   @ModelAttribute("myData")
    public String myData() {
        return myData;
    }

Or for instance, if you want to replace a set of constants:

@Component
public class ApplicationPaths {
    private final static String MAIN = "/main";
    private final static String ORDERS = "/orders";
    
    public String getMain() {
        return MAIN;
    }
    
    public String getOrders() {
        return ORDERS;
    }
    
    ...
    
}

@Controller
pubilc class SomeController {

   @ModelAttribute("appPaths")
    public String appPaths() {
        return this.appPaths; // previously injected
    }

    ...

}

@abs0
Copy link

abs0 commented Apr 26, 2021

I absolutely understand the rationale behind the changes, and we're more than happy to adjust our code to access enums in a safer way, but I have to confess "Go add a getter to a model object for every enum you might ever want to use in a template" is not the answer for which I was hoping.

It would be nice if thymeleaf could provide a recommended way of managing enum visibility - possibly even passing in a list of enums to make available in some form to the templates.

@leonard84
Copy link

IMHO it would be better to be able to add classes to an AllowList. Those classes are allowed for static access in those sensitive areas. This way we can allow access to our enums and constant classes. It would be a similar compromise to what has been done with the serialization vulnerabilities.

@jochenchrist
Copy link

A version bump would have been nice.

@scoldwell
Copy link
Author

I complete understand the idea of preventing execution of arbitrary code, but in most of the cases myself and others have raised, we're talking about constants/enums. Could code be added to the detection routine to determine if the target of the T() expression is a constant and allow that?

@membersound
Copy link

I really wonder why there is no mention of the alternatives.
I ended up using with th:with like below

th:with="rptBy=${T(com.test.enum.ReportBy).BY_G1}"
th:data-groupby="${rptBy}"

I can encourage you using this quickfix for your broken definitions.
I only had to use a bunch of it, and can now work again with the most recent version again.

Martin-BG added a commit to Martin-BG/Marketplace that referenced this issue Jul 18, 2021
@justwiebe
Copy link

I feel like a much less breaking way would be to instead not execute expressions in string variables rather than to stop developers from explicitly calling an enum. This is really annoying

@sagarnishant1
Copy link

If you are working with Spring then another workaround to this is to define global model attributes using @ControllerAdvice. For eg,

@ControllerAdvice
public class GlobalModelAttribute {

    @ModelAttribute("myEnum1List")
    public String myEnum1List() {
        return enumList;
    }

    @ModelAttribute("constantList")
    public String constantList() {
        return constantList;
    }

    @ModelAttribute("aConstant")
    public String aConstant() {
        return aConstant;
    }
}

An example at below link
https://stackoverflow.com/questions/24937441/comparing-the-enum-constants-in-thymeleaf/34354605#34354605

@diw
Copy link

diw commented Oct 21, 2022 via email

@rickgong
Copy link

rickgong commented Nov 26, 2023

This is my solution, it worked, no need to modify template.

    @Bean
    public TemplateDialect templateDialect() {
        return new TemplateDialect();
    }
class TemplateDialect extends AbstractDialect implements IExpressionObjectDialect {

    public TemplateDialect() {
        super(TemplateDialect.class.getName());
    }

    @Override
    public IExpressionObjectFactory getExpressionObjectFactory() {
        return new IExpressionObjectFactory() {

            @Override
            public Set<String> getAllExpressionObjectNames() {
                Set<String> set = new LinkedHashSet<>();
                set.add("request");
                set.add("response");
                set.add("session");
                set.add("servletContext");
                return set;
            }

            @Override
            public Object buildObject(IExpressionContext context, String expressionObjectName) {
                if ("request".equals(expressionObjectName)) {
                    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                }
                if ("response".equals(expressionObjectName)) {
                    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
                }
                if ("session".equals(expressionObjectName)) {
                    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession(false);
                }
                if ("servletContext".equals(expressionObjectName)) {
                    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getServletContext();
                }
                throw new UnsupportedOperationException("unsupported expression");
            }

            @Override
            public boolean isCacheable(String expressionObjectName) {
                return true;
            }
        };
    }
}


@Alaincruzc90
Copy link

So, I can't migrate my Spring Boot application, since my templates use static classes (enums) extensible. Migrating would take a long time and even them, some things might failed when deploying to production. And when using Spring Boot 3.X I can't even downgrade to Thymeleaf 3.0.11.RELEASE, because of the thymeleaf-Spring6 dependency.

So, has anyone find a workaround that would allowed the calling of at least some static classes? That way there won't be a need to modify the templates.

This is my solution, it worked, no need to modify template.

    @Bean
    public TemplateDialect templateDialect() {
        return new TemplateDialect();
    }
class TemplateDialect extends AbstractDialect implements IExpressionObjectDialect {

    public TemplateDialect() {
        super(TemplateDialect.class.getName());
    }

    @Override
    public IExpressionObjectFactory getExpressionObjectFactory() {
        return new IExpressionObjectFactory() {

            @Override
            public Set<String> getAllExpressionObjectNames() {
                Set<String> set = new LinkedHashSet<>();
                set.add("request");
                set.add("response");
                set.add("session");
                set.add("servletContext");
                return set;
            }

            @Override
            public Object buildObject(IExpressionContext context, String expressionObjectName) {
                if ("request".equals(expressionObjectName)) {
                    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                }
                if ("response".equals(expressionObjectName)) {
                    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
                }
                if ("session".equals(expressionObjectName)) {
                    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession(false);
                }
                if ("servletContext".equals(expressionObjectName)) {
                    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getServletContext();
                }
                throw new UnsupportedOperationException("unsupported expression");
            }

            @Override
            public boolean isCacheable(String expressionObjectName) {
                return true;
            }
        };
    }
}

Also, sadly this solution didn't work with my configuration.

@duoduobingbing
Copy link

duoduobingbing commented May 14, 2024

So, I can't migrate my Spring Boot application, since my templates use static classes (enums) extensible. Migrating would take a long time and even them, some things might failed when deploying to production. And when using Spring Boot 3.X I can't even downgrade to Thymeleaf 3.0.11.RELEASE, because of the thymeleaf-Spring6 dependency.

So, has anyone find a workaround that would allowed the calling of at least some static classes? That way there won't be a need to modify the templates.

[...]

Also, sadly this solution didn't work with my configuration.

As mentioned in #829 there is currently no way to configure the allowance behavior. Last year I opened a PR (PR#960) to alleviate this but it has not gotten any traction as of now.

@rickgong
Copy link

Today i tested with Springboot 3.3.0, it still worked on my end.

1. First, it requires a Configuration.

@Configuration
public class TemplateConfiguration {
    @Bean
    public TemplateDialect templateDialect() {
        return new TemplateDialect();
    }
}

Put the configuration FQN to src/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.

2. The class of TemplateDialect.
Note I added httpServletRequest and httpServletResponse in the dialect, which is important to be compatible with legacy Thymelef templates.

class TemplateDialect extends AbstractDialect implements IExpressionObjectDialect {

    public TemplateDialect() {
        super(TemplateDialect.class.getName());
    }

    @Override
    public IExpressionObjectFactory getExpressionObjectFactory() {
        return new IExpressionObjectFactory() {

            @Override
            public Set<String> getAllExpressionObjectNames() {
                return Collections.unmodifiableSet(setOf("request", "httpServletRequest", "response", "httpServletResponse"));
            }

            @Override
            public Object buildObject(IExpressionContext context, String expressionObjectName) {
                if ("request".equals(expressionObjectName) || "httpServletRequest".equals(expressionObjectName)) {
                    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                }
                if ("response".equals(expressionObjectName) || "httpServletResponse".equals(expressionObjectName)) {
                    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
                }
                throw new UnsupportedOperationException("unsupported expression");
            }

            @Override
            public boolean isCacheable(String expressionObjectName) {
                return true;
            }
        };
    }
}

3. Thymeleaf example.

<script type="text/javascript" th:inline="javascript">
    var configUrlValue = [[${#httpServletRequest.getAttribute('configUrl')}]];
</script>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests