This is an automated email from the ASF dual-hosted git repository. jsedding pushed a commit to branch jsedding/SLING-12366-read-inputstream-from-closed-session in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-xss.git
commit 82b3aafaad3d6c4feb971cefb30d79d29f5eff03 Author: Julian Sedding <jsedd...@apache.org> AuthorDate: Tue Jul 2 12:46:34 2024 +0200 SLING-12366 - Failure to read from InputStream backed by closed session - fix failing test case --- .../org/apache/sling/xss/impl/XSSFilterImpl.java | 174 +++++++++++---------- .../XSSProtectionAPIWebConsolePlugin.java | 10 +- 2 files changed, 100 insertions(+), 84 deletions(-) diff --git a/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java b/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java index 98f4c3d..606716e 100644 --- a/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java +++ b/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java @@ -16,7 +16,10 @@ ******************************************************************************/ package org.apache.sling.xss.impl; +import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -25,8 +28,11 @@ import java.util.Dictionary; import java.util.Hashtable; import java.util.List; import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.regex.Pattern; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringEscapeUtils; import org.apache.commons.text.translate.NumericEntityUnescaper; @@ -37,8 +43,6 @@ import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.api.resource.observation.ExternalResourceChangeListener; import org.apache.sling.api.resource.observation.ResourceChange; import org.apache.sling.api.resource.observation.ResourceChangeListener; -import org.apache.sling.commons.metrics.Counter; -import org.apache.sling.commons.metrics.MetricsService; import org.apache.sling.serviceusermapping.ServiceUserMapped; import org.apache.sling.xss.ProtectionContext; import org.apache.sling.xss.XSSFilter; @@ -47,6 +51,7 @@ import org.apache.sling.xss.impl.xml.Attribute; import org.apache.sling.xss.impl.xml.Regexp; import org.apache.sling.xss.impl.xml.Tag; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.osgi.framework.ServiceRegistration; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -88,7 +93,7 @@ public class XSSFilterImpl implements XSSFilter { } - private final Logger logger = LoggerFactory.getLogger(XSSFilterImpl.class); + private static final Logger logger = LoggerFactory.getLogger(XSSFilterImpl.class); public static final String ALPHA = "(?:\\p{L}\\p{M}*)"; public static final String HEX_DIGIT = "\\p{XDigit}"; @@ -175,7 +180,6 @@ public class XSSFilterImpl implements XSSFilter { private final XSSFilterRule plainHtmlContext = new PlainTextToHtmlContentContext(); private volatile AntiSamyPolicy activePolicy; - private volatile PolicyHandler policyHandler; @Reference private ResourceResolverFactory resourceResolverFactory; @@ -194,7 +198,7 @@ public class XSSFilterImpl implements XSSFilter { @Override public boolean check(final ProtectionContext context, final String src) { final XSSFilterRule ctx = this.getFilterRule(context); - return ctx.check(policyHandler, src); + return ctx.check(getPolicyHandler(), src); } @Override @@ -205,7 +209,13 @@ public class XSSFilterImpl implements XSSFilter { @Override public String filter(final ProtectionContext context, final String src) { final XSSFilterRule ctx = this.getFilterRule(context); - return ctx.filter(policyHandler, src); + return ctx.filter(getPolicyHandler(), src); + } + + private @Nullable PolicyHandler getPolicyHandler() { + return Optional.ofNullable(getActivePolicy()) + .map(AntiSamyPolicy::getPolicyHandler) + .orElse(null); } @Override @@ -235,6 +245,13 @@ public class XSSFilterImpl implements XSSFilter { return activePolicy; } + public void writeActivePolicyConfig(OutputStream outputStream) { + withPolicyResource(policyResource -> { + activePolicy.writeConfig(policyResource, outputStream); + return null; + }); + } + private boolean runHrefValidation(@NotNull String url) { // Same logic as in org.owasp.validator.html.scan.MagicSAXFilter.startElement() String urlLowerCase = url.toLowerCase(); @@ -266,7 +283,7 @@ public class XSSFilterImpl implements XSSFilter { protected void activate(ComponentContext componentContext, Configuration configuration) { // load default handler policyPath = configuration.policyPath(); - updatePolicy(); + updateActivePolicy(); if (serviceRegistration != null) { serviceRegistration.unregister(); } @@ -285,64 +302,32 @@ public class XSSFilterImpl implements XSSFilter { } } - synchronized void updatePolicy() { - this.policyHandler = null; - try (final ResourceResolver xssResourceResolver = resourceResolverFactory.getServiceResourceResolver(null)) { - Resource policyResource = xssResourceResolver.getResource(policyPath); - if (policyResource != null) { - setActivePolicy(policyResource); - } - } catch (final LoginException e) { - logger.error("Unable to load the default policy file.", e); - } - if (policyHandler == null) { + synchronized void updateActivePolicy() { + final AntiSamyPolicy originalActivePolicy = this.activePolicy; + this.activePolicy = withPolicyResource(AntiSamyPolicy::create); + if (activePolicy == originalActivePolicy) { // the content was not installed but the service is active; let's use the embedded file for the default handler - setActiveEmbededPolicy(); + this.activePolicy = AntiSamyPolicy.createEmbedded(); } - if (policyHandler == null || activePolicy == null) { + if (activePolicy == originalActivePolicy) { + activePolicy = null; throw new IllegalStateException("Cannot load a policy handler."); } + updatePolicyHandler(activePolicy.getPolicyHandler()); } - private void setActiveEmbededPolicy() { - // the content was not installed but the service is active; let's use the embedded file for the default handler - logger.info("Could not find a policy file at the configured location {}. Attempting to use the default resource embedded in" + - " the bundle.", policyPath); - activePolicy = new AntiSamyPolicy(true, EMBEDDED_POLICY_PATH); - try (InputStream policyStream = activePolicy.read()) { - setPolicyHandler(new PolicyHandler(policyStream)); - logger.info("Installed policy from the embedded {} file from the bundle.", activePolicy.path); - } catch (Exception e) { - activePolicy = null; - Throwable[] suppressed = e.getSuppressed(); - if (suppressed.length > 0) { - for (Throwable t : suppressed) { - logger.error("Unable to load policy from embedded policy file.", t); - } - } - logger.error("Unable to load policy from embedded policy file.", e); - } - - } - - private void setActivePolicy(Resource policyResource) { - activePolicy = new AntiSamyPolicy(false, policyResource.getPath()); - try (InputStream policyStream = activePolicy.read()) { - setPolicyHandler(new PolicyHandler(policyStream)); - logger.info("Installed policy from {}.", activePolicy.path); - } catch (Exception e) { - activePolicy = null; - Throwable[] suppressed = e.getSuppressed(); - if (suppressed.length > 0) { - for (Throwable t : suppressed) { - logger.error("Unable to load policy from " + policyResource.getPath(), t); - } + private <T> T withPolicyResource(Function<Resource, T> mapper) { + try (final ResourceResolver xssResourceResolver = resourceResolverFactory.getServiceResourceResolver(null)) { + Resource policyResource = xssResourceResolver.getResource(policyPath); + if (policyResource != null) { + return mapper.apply(policyResource); } - logger.error("Unable to load policy from " + policyResource.getPath(), e); + } catch (final LoginException e) { + logger.error("Unable to load the default policy file.", e); } + return null; } - /** * Get the filter rule context. */ @@ -356,15 +341,13 @@ public class XSSFilterImpl implements XSSFilter { return this.plainHtmlContext; } - private void setPolicyHandler(PolicyHandler policyHandler) { + private void updatePolicyHandler(PolicyHandler policyHandler) { Tag linkTag = policyHandler.getPolicy().getTagRules().get("a"); hrefAttribute = (linkTag != null) ? linkTag.getAttributeByName("href") : null; if (hrefAttribute == null) { // Fallback to default configuration hrefAttribute = DEFAULT_HREF_ATTRIBUTE; } - - this.policyHandler = policyHandler; } private class PolicyChangeListener implements ResourceChangeListener, ExternalResourceChangeListener { @@ -373,43 +356,76 @@ public class XSSFilterImpl implements XSSFilter { for (ResourceChange change : resourceChanges) { if (change.getPath().endsWith(policyPath)) { logger.info("Detected policy file change ({}) at {}. Updating policy handler.", change.getType().name(), change.getPath()); - updatePolicy(); + updateActivePolicy(); } } } } - public class AntiSamyPolicy { - private final boolean embedded; - private final String path; + public static class AntiSamyPolicy { + + private final String policyPath; + + private final PolicyHandler policyHandler; - AntiSamyPolicy(boolean embedded, String path) { - this.embedded = embedded; - this.path = path; + public static AntiSamyPolicy create(Resource policyResource) { + String policyPath = policyResource.getPath(); + return createAntiSamyPolicy(policyPath, () -> streamResource(policyResource)); } - public InputStream read() { - if (embedded) { - return this.getClass().getClassLoader().getResourceAsStream(EMBEDDED_POLICY_PATH); - } - try (final ResourceResolver xssResourceResolver = resourceResolverFactory.getServiceResourceResolver(null)) { - Resource policyResource = xssResourceResolver.getResource(path); - if (policyResource != null) { - return policyResource.adaptTo(InputStream.class); - } else { - throw new IllegalStateException("Cannot read policy from " + path + "."); + public static AntiSamyPolicy createEmbedded() { + return createAntiSamyPolicy(null, AntiSamyPolicy::streamEmbedded); + } + + private static AntiSamyPolicy createAntiSamyPolicy(@Nullable String policyPath, @NotNull Supplier<InputStream> policySupplier) { + String pathName = policyPath == null ? "embedded policy file" : policyPath; + try (InputStream policyStream = policySupplier.get()) { + PolicyHandler policyHandler = new PolicyHandler(policyStream); + logger.info("Installed policy from {}.", pathName); + return new AntiSamyPolicy(policyHandler, policyPath); + } catch (Exception e) { + Throwable[] suppressed = e.getSuppressed(); + for (Throwable t : suppressed) { + logger.error("Unable to load policy from {}.", pathName, t); } - } catch (LoginException e) { - throw new IllegalStateException("Cannot read policy.", e); + logger.error("Unable to load policy from {}.", pathName, e); + return null; } } + private AntiSamyPolicy(@NotNull PolicyHandler policyHandler, @Nullable String policyPath) { + this.policyPath = policyPath; + this.policyHandler = policyHandler; + } + public boolean isEmbedded() { - return embedded; + return policyPath == null; } public String getPath() { - return path; + return isEmbedded() ? EMBEDDED_POLICY_PATH : policyPath; + } + + public PolicyHandler getPolicyHandler() { + return policyHandler; + } + + public void writeConfig(Resource policyResource, OutputStream outputStream) { + try (InputStream inputStream = isEmbedded() ? streamEmbedded() : streamResource(policyResource)) { + if (inputStream != null) { + IOUtils.copy(inputStream, outputStream); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static @Nullable InputStream streamResource(Resource policyResource) { + return policyResource.adaptTo(InputStream.class); + } + + private static @Nullable InputStream streamEmbedded() { + return XSSFilterImpl.class.getClassLoader().getResourceAsStream(EMBEDDED_POLICY_PATH); } } } diff --git a/src/main/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePlugin.java b/src/main/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePlugin.java index c389d10..536cfe4 100644 --- a/src/main/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePlugin.java +++ b/src/main/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePlugin.java @@ -18,6 +18,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ package org.apache.sling.xss.impl.webconsole; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; @@ -148,7 +149,7 @@ public class XSSProtectionAPIWebConsolePlugin extends HttpServlet { response.setContentType("application/xml"); response.setHeader("Content-Disposition", "attachment; filename=config.xml"); XSSFilterImpl xssFilterImpl = (XSSFilterImpl) xssFilter; - IOUtils.copy(xssFilterImpl.getActivePolicy().read(), response.getOutputStream()); + xssFilterImpl.writeActivePolicyConfig(response.getOutputStream()); } catch (IOException e) { LOGGER.error("Unable to stream AntiSamy configuration.", e); } @@ -189,11 +190,10 @@ public class XSSProtectionAPIWebConsolePlugin extends HttpServlet { printWriter.printf("is loaded from %s.", antiSamyPolicy.getPath()); } printWriter.write("<button style='float:right' type='button' id='download-config'>Download</button></p>"); - String contents = ""; - try (InputStream configurationStream = antiSamyPolicy.read()) { - contents = IOUtils.toString(configurationStream, StandardCharsets.UTF_8); - } printWriter.write("<pre class='prettyprint linenums'>"); + ByteArrayOutputStream configStream = new ByteArrayOutputStream(); + xssFilterImpl.writeActivePolicyConfig(configStream); + String contents = new String(configStream.toByteArray(), StandardCharsets.UTF_8); printWriter.write(StringEscapeUtils.escapeHtml4(contents)); printWriter.write("</pre>"); printWriter.write("</div>");