From a87dedd916314bc81fa319adc6a024b14bf81c92 Mon Sep 17 00:00:00 2001 From: John Niang Date: Fri, 20 Sep 2024 11:14:58 +0800 Subject: [PATCH] Make ApplicationContext inaccessible in ITemplateContext (#6680) #### What type of PR is this? /kind improvement /area core /area plugin /milestone 2.20.x #### What this PR does / why we need it: This PR disables access to ApplicationContext using ITemplateContext. #### Does this PR introduce a user-facing change? ```release-note None ``` --- .../dialect/CommentElementTagProcessor.java | 2 +- .../dialect/GlobalHeadInjectionProcessor.java | 4 +- .../dialect/HaloPostTemplateHandler.java | 3 +- .../theme/dialect/SecureTemplateContext.java | 159 ++++++++++++++++++ .../TemplateFooterElementTagProcessor.java | 4 +- .../dialect/HaloPostTemplateHandlerTest.java | 10 +- 6 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 application/src/main/java/run/halo/app/theme/dialect/SecureTemplateContext.java diff --git a/application/src/main/java/run/halo/app/theme/dialect/CommentElementTagProcessor.java b/application/src/main/java/run/halo/app/theme/dialect/CommentElementTagProcessor.java index 96b9935c1e..f449f2e696 100644 --- a/application/src/main/java/run/halo/app/theme/dialect/CommentElementTagProcessor.java +++ b/application/src/main/java/run/halo/app/theme/dialect/CommentElementTagProcessor.java @@ -45,6 +45,6 @@ protected void doProcess(ITemplateContext context, IProcessableElementTag tag, structureHandler.replaceWith("", false); return; } - commentWidget.render(context, tag, structureHandler); + commentWidget.render(new SecureTemplateContext(context), tag, structureHandler); } } diff --git a/application/src/main/java/run/halo/app/theme/dialect/GlobalHeadInjectionProcessor.java b/application/src/main/java/run/halo/app/theme/dialect/GlobalHeadInjectionProcessor.java index 6be5c52bbf..a346d42e0b 100644 --- a/application/src/main/java/run/halo/app/theme/dialect/GlobalHeadInjectionProcessor.java +++ b/application/src/main/java/run/halo/app/theme/dialect/GlobalHeadInjectionProcessor.java @@ -71,7 +71,9 @@ protected void doProcess(ITemplateContext context, IModel model, // apply processors to modelToInsert getTemplateHeadProcessors(context) - .concatMap(processor -> processor.process(context, modelToInsert, structureHandler)) + .concatMap(processor -> processor.process( + new SecureTemplateContext(context), modelToInsert, structureHandler) + ) .then() .block(); diff --git a/application/src/main/java/run/halo/app/theme/dialect/HaloPostTemplateHandler.java b/application/src/main/java/run/halo/app/theme/dialect/HaloPostTemplateHandler.java index 1eebd67fea..25755a457a 100644 --- a/application/src/main/java/run/halo/app/theme/dialect/HaloPostTemplateHandler.java +++ b/application/src/main/java/run/halo/app/theme/dialect/HaloPostTemplateHandler.java @@ -57,7 +57,8 @@ private IProcessableElementTag handleElementTag( var context = getContext(); for (ElementTagPostProcessor elementTagPostProcessor : postProcessors) { tagProcessorChain = tagProcessorChain.flatMap( - tag -> elementTagPostProcessor.process(context, tag).defaultIfEmpty(tag) + tag -> elementTagPostProcessor.process(new SecureTemplateContext(context), tag) + .defaultIfEmpty(tag) ); } processedTag = diff --git a/application/src/main/java/run/halo/app/theme/dialect/SecureTemplateContext.java b/application/src/main/java/run/halo/app/theme/dialect/SecureTemplateContext.java new file mode 100644 index 0000000000..3fe8d04036 --- /dev/null +++ b/application/src/main/java/run/halo/app/theme/dialect/SecureTemplateContext.java @@ -0,0 +1,159 @@ +package run.halo.app.theme.dialect; + +import static org.thymeleaf.spring6.expression.ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.thymeleaf.IEngineConfiguration; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.context.IdentifierSequences; +import org.thymeleaf.engine.TemplateData; +import org.thymeleaf.expression.IExpressionObjects; +import org.thymeleaf.inline.IInliner; +import org.thymeleaf.model.IModelFactory; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.templatemode.TemplateMode; + +/** + * Secure template context. + * + * @author johnniang + * @since 2.20.0 + */ +class SecureTemplateContext implements ITemplateContext { + + private static final Set DANGEROUS_VARIABLES = + Set.of(THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME); + + private final ITemplateContext delegate; + + public SecureTemplateContext(ITemplateContext delegate) { + this.delegate = delegate; + } + + @Override + public TemplateData getTemplateData() { + return delegate.getTemplateData(); + } + + @Override + public TemplateMode getTemplateMode() { + return delegate.getTemplateMode(); + } + + @Override + public List getTemplateStack() { + return delegate.getTemplateStack(); + } + + @Override + public List getElementStack() { + return delegate.getElementStack(); + } + + @Override + public Map getTemplateResolutionAttributes() { + return delegate.getTemplateResolutionAttributes(); + } + + @Override + public IModelFactory getModelFactory() { + return delegate.getModelFactory(); + } + + @Override + public boolean hasSelectionTarget() { + return delegate.hasSelectionTarget(); + } + + @Override + public Object getSelectionTarget() { + return delegate.getSelectionTarget(); + } + + @Override + public IInliner getInliner() { + return delegate.getInliner(); + } + + @Override + public String getMessage( + Class origin, + String key, + Object[] messageParameters, + boolean useAbsentMessageRepresentation + ) { + return delegate.getMessage(origin, key, messageParameters, useAbsentMessageRepresentation); + } + + @Override + public String buildLink(String base, Map parameters) { + return delegate.buildLink(base, parameters); + } + + @Override + public IdentifierSequences getIdentifierSequences() { + return delegate.getIdentifierSequences(); + } + + @Override + public IEngineConfiguration getConfiguration() { + return delegate.getConfiguration(); + } + + @Override + public IExpressionObjects getExpressionObjects() { + return delegate.getExpressionObjects(); + } + + @Override + public Locale getLocale() { + return delegate.getLocale(); + } + + @Override + public boolean containsVariable(String name) { + if (DANGEROUS_VARIABLES.contains(name)) { + return false; + } + return delegate.containsVariable(name); + } + + @Override + public Set getVariableNames() { + return delegate.getVariableNames() + .stream() + .filter(name -> !DANGEROUS_VARIABLES.contains(name)) + .collect(Collectors.toSet()); + } + + @Override + public Object getVariable(String name) { + if (DANGEROUS_VARIABLES.contains(name)) { + return null; + } + return delegate.getVariable(name); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SecureTemplateContext that = (SecureTemplateContext) o; + return Objects.equals(delegate, that.delegate); + } + + @Override + public int hashCode() { + return Objects.hashCode(delegate); + } +} diff --git a/application/src/main/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessor.java b/application/src/main/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessor.java index 74239b1f2e..26a286b26a 100644 --- a/application/src/main/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessor.java +++ b/application/src/main/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessor.java @@ -58,8 +58,8 @@ protected void doProcess(ITemplateContext context, IProcessableElementTag tag, modelToInsert.add(context.getModelFactory().createText(globalFooterText)); getTemplateFooterProcessors(context) - .concatMap(processor -> processor.process(context, tag, - structureHandler, modelToInsert) + .concatMap(processor -> processor.process( + new SecureTemplateContext(context), tag, structureHandler, modelToInsert) ) .then() .block(); diff --git a/application/src/test/java/run/halo/app/theme/dialect/HaloPostTemplateHandlerTest.java b/application/src/test/java/run/halo/app/theme/dialect/HaloPostTemplateHandlerTest.java index 20c32f90b3..9d6266f2d7 100644 --- a/application/src/test/java/run/halo/app/theme/dialect/HaloPostTemplateHandlerTest.java +++ b/application/src/test/java/run/halo/app/theme/dialect/HaloPostTemplateHandlerTest.java @@ -82,7 +82,7 @@ void shouldHandleStandaloneElementIfNoElementTagProcessors( void shouldHandleStandaloneElementIfOneElementTagProcessorProvided() { var processor = mock(ElementTagPostProcessor.class); var newTag = mock(IStandaloneElementTag.class); - when(processor.process(templateContext, standaloneElementTag)) + when(processor.process(new SecureTemplateContext(templateContext), standaloneElementTag)) .thenReturn(Mono.just(newTag)); when(extensionGetter.getExtensionList(ElementTagPostProcessor.class)) .thenReturn(List.of(processor)); @@ -97,7 +97,7 @@ void shouldHandleStandaloneElementIfOneElementTagProcessorProvided() { void shouldHandleStandaloneElementIfTagTypeChanged() { var processor = mock(ElementTagPostProcessor.class); var newTag = mock(IStandaloneElementTag.class); - when(processor.process(templateContext, standaloneElementTag)) + when(processor.process(new SecureTemplateContext(templateContext), standaloneElementTag)) .thenReturn(Mono.just(newTag)); when(extensionGetter.getExtensionList(ElementTagPostProcessor.class)) .thenReturn(List.of(processor)); @@ -114,9 +114,9 @@ void shouldHandleStandaloneElementIfMoreElementTagProcessorsProvided() { var processor2 = mock(ElementTagPostProcessor.class); var newTag1 = mock(IStandaloneElementTag.class); var newTag2 = mock(IStandaloneElementTag.class); - when(processor1.process(templateContext, standaloneElementTag)) + when(processor1.process(new SecureTemplateContext(templateContext), standaloneElementTag)) .thenReturn(Mono.just(newTag1)); - when(processor2.process(templateContext, newTag1)) + when(processor2.process(new SecureTemplateContext(templateContext), newTag1)) .thenReturn(Mono.just(newTag2)); when(extensionGetter.getExtensionList(ElementTagPostProcessor.class)) .thenReturn(List.of(processor1, processor2)); @@ -131,7 +131,7 @@ void shouldHandleStandaloneElementIfMoreElementTagProcessorsProvided() { void shouldNotHandleIfProcessedTagTypeChanged() { var processor = mock(ElementTagPostProcessor.class); var newTag = mock(IOpenElementTag.class); - when(processor.process(templateContext, standaloneElementTag)) + when(processor.process(new SecureTemplateContext(templateContext), standaloneElementTag)) .thenReturn(Mono.just(newTag)); when(extensionGetter.getExtensionList(ElementTagPostProcessor.class)) .thenReturn(List.of(processor));