SLING-10147 block unauthorized access to SlingBindingsVariablesListJsonServlet (#7)
scripting variables implementation details were exposed to not authorized users
diff --git a/pom.xml b/pom.xml
index fdac974..c813149 100644
--- a/pom.xml
+++ b/pom.xml
@@ -272,6 +272,13 @@
</dependency>
<dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient-osgi</artifactId>
+ <version>4.5.8</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.testing.osgi-mock</artifactId>
<version>2.3.6</version>
@@ -376,5 +383,11 @@
<version>1.2.6</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.extensions.webconsolesecurityprovider</artifactId>
+ <version>1.2.4</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/src/main/java/org/apache/sling/scripting/core/impl/SlingBindingsVariablesListJsonServlet.java b/src/main/java/org/apache/sling/scripting/core/impl/SlingBindingsVariablesListJsonServlet.java
index 7e99689..76ca403 100644
--- a/src/main/java/org/apache/sling/scripting/core/impl/SlingBindingsVariablesListJsonServlet.java
+++ b/src/main/java/org/apache/sling/scripting/core/impl/SlingBindingsVariablesListJsonServlet.java
@@ -26,8 +26,11 @@
import javax.script.ScriptEngineManager;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletResponse;
import org.apache.felix.utils.json.JSONWriter;
+import org.apache.felix.webconsole.WebConsoleSecurityProvider;
+import org.apache.felix.webconsole.WebConsoleSecurityProvider2;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.NonExistingResource;
@@ -43,6 +46,8 @@
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
/**
* Return all scripting variables for all registered scripting languages for the default context (=request).
@@ -68,6 +73,12 @@
private static final long serialVersionUID = -6744726829737263875L;
/**
+ * The webconsole security provider
+ */
+ @Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY)
+ private WebConsoleSecurityProvider webconsoleSecurity;
+
+ /**
* The script engine manager.
*/
@Reference
@@ -91,6 +102,27 @@
@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
+ boolean allowed = true;
+ if (webconsoleSecurity == null) {
+ log("Access forbidden as the WebConsoleSecurity reference is not set");
+ allowed = false;
+ } else if (!(webconsoleSecurity instanceof WebConsoleSecurityProvider2)) {
+ log("Access forbidden as the WebConsoleSecurity reference does not implement WebConsoleSecurityProvider2");
+ allowed = false;
+ } else if (!((WebConsoleSecurityProvider2)webconsoleSecurity).authenticate(request, response)) {
+ log("Access forbidden as the WebConsoleSecurity component returned false");
+ // the request is terminated without any more response sent back to the client.
+ // The WebConsoleSecurityProvider2 implementation may have sent auth challenge to the client
+ // in the case of anonymous access.
+ allowed = false;
+ }
+ if (!allowed) {
+ if (!response.isCommitted()) {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ }
+ return;
+ }
+
response.setContentType("application/json");
JSONWriter jsonWriter = new JSONWriter(response.getWriter());
jsonWriter.array();
diff --git a/src/test/java/org/apache/sling/scripting/core/it/SLING_10147IT.java b/src/test/java/org/apache/sling/scripting/core/it/SLING_10147IT.java
new file mode 100644
index 0000000..4312e36
--- /dev/null
+++ b/src/test/java/org/apache/sling/scripting/core/it/SLING_10147IT.java
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.scripting.core.it;
+
+import static org.apache.sling.testing.paxexam.SlingOptions.slingAuthForm;
+import static org.apache.sling.testing.paxexam.SlingOptions.slingQuickstartOakTar;
+import static org.apache.sling.testing.paxexam.SlingOptions.slingScriptingJavascript;
+import static org.apache.sling.testing.paxexam.SlingOptions.versionResolver;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.ops4j.pax.exam.CoreOptions.composite;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.vmOption;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.factoryConfiguration;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.concurrent.Callable;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.auth.AuthenticationException;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.LaxRedirectStrategy;
+import org.awaitility.Awaitility;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.rules.TestRule;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.options.ModifiableCompositeOption;
+import org.ops4j.pax.exam.options.extra.VMOption;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tests for SLING-10147 - verify scripting variables implementation details are not
+ * exposed to not authorized users
+ */
+@RunWith(Enclosed.class)
+public class SLING_10147IT {
+
+ private SLING_10147IT() {
+ // private constructor to hide the implicit public one
+ }
+
+ /**
+ * Base class to share the common parts of LoadedIT and NotLoadedIT
+ * tests
+ */
+ public abstract static class BaseIT extends ScriptingCoreTestSupport {
+ protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+ protected CloseableHttpClient httpClient;
+
+ @Rule
+ public TestRule watcher = new TestWatcher() {
+
+ /* (non-Javadoc)
+ * @see org.junit.rules.TestWatcher#starting(org.junit.runner.Description)
+ */
+ @Override
+ protected void starting(Description description) {
+ logger.info("Starting test: {}", description.getMethodName());
+ }
+
+ /* (non-Javadoc)
+ * @see org.junit.rules.TestWatcher#finished(org.junit.runner.Description)
+ */
+ @Override
+ protected void finished(Description description) {
+ logger.info("Finished test: {}", description.getMethodName());
+ }
+
+ };
+
+ @Before
+ public void before() {
+ waitForServices();
+
+ int timeout = 15; // seconds
+ RequestConfig config = RequestConfig.custom()
+ .setConnectTimeout(timeout * 1000)
+ .setConnectionRequestTimeout(timeout * 1000)
+ .setSocketTimeout(timeout * 1000).build();
+
+ HttpClientBuilder httpClientBuilder = HttpClientBuilder.create()
+ .setRedirectStrategy(new LaxRedirectStrategy())
+ .setDefaultRequestConfig(config);
+ httpClient = httpClientBuilder.build();
+ }
+
+ /**
+ * Wait for services to be available
+ */
+ protected void waitForServices() {
+ final BundleContext bundleContext = FrameworkUtil.getBundle(SLING_10147IT.class).getBundleContext();
+ Awaitility.await()
+ .atMost(Duration.ofMinutes(5))
+ .pollInterval(Duration.ofSeconds(5))
+ .until(new Callable<Boolean>() {
+
+ private String [] servicesLists = new String[] {
+ "org.apache.sling.jcr.api.SlingRepository",
+ "org.apache.sling.engine.auth.Authenticator",
+ "org.apache.sling.api.resource.ResourceResolverFactory",
+ "org.apache.sling.api.servlets.ServletResolver",
+ "javax.script.ScriptEngineManager"
+ };
+
+ @Override
+ public Boolean call() throws Exception {
+ boolean foundAllServices = true;
+ for (String serviceClass : servicesLists) {
+ ServiceReference<?> serviceReference = bundleContext.getServiceReference(serviceClass);
+ if (serviceReference == null) {
+ foundAllServices = false;
+ break;
+ }
+ }
+ return foundAllServices;
+ }
+
+ });
+ }
+
+ @After
+ public void after() throws IOException {
+ if (httpClient != null) {
+ httpClient.close();
+ httpClient = null;
+ }
+ }
+
+ @Configuration
+ public Option[] configuration() {
+ versionResolver.setVersionFromProject("org.apache.sling", "org.apache.sling.api");
+ versionResolver.setVersionFromProject("org.apache.sling", "org.apache.sling.resourceresolver");
+ versionResolver.setVersionFromProject("org.apache.sling", "org.apache.sling.servlets.resolver");
+ versionResolver.setVersionFromProject("org.apache.sling", "org.apache.sling.scripting.api");
+
+ final Option webconsolesecurityprovider = mavenBundle().groupId("org.apache.sling").artifactId("org.apache.sling.extensions.webconsolesecurityprovider").versionAsInProject();
+ return composite(
+ slingQuickstartOakTar(String.format("target/%s", getClass().getSimpleName()), httpPort),
+ slingScriptingJavascript(),
+ slingAuthForm(),
+ baseConfiguration(),
+ mavenBundle().groupId("org.apache.httpcomponents").artifactId("httpcore-osgi").version(versionResolver),
+ mavenBundle().groupId("org.apache.httpcomponents").artifactId("httpclient-osgi").version(versionResolver),
+ webconsolesecurityprovider,
+ optionalRemoteDebug()
+ ).remove(
+ scriptingCore // remove the old version
+ ).getOptions();
+ }
+
+ /**
+ * Optionally configure remote debugging on the port supplied by the "debugPort"
+ * system property.
+ */
+ protected ModifiableCompositeOption optionalRemoteDebug() {
+ VMOption option = null;
+ String property = System.getProperty("debugPort");
+ if (property != null) {
+ option = vmOption(String.format("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=%s", property));
+ }
+ return composite(option);
+ }
+
+ protected void checkContentType(CloseableHttpResponse response ,String expected) {
+ // Remove whatever follows semicolon in content-type
+ HttpEntity entity = response.getEntity();
+ Header contentTypeHeader = entity.getContentType();
+ String contentType = contentTypeHeader == null ? null : contentTypeHeader.getValue();
+ if (contentType != null) {
+ contentType = contentType.split(";")[0].trim();
+ }
+ // check for match
+ assertEquals(expected, contentType);
+ }
+
+ }
+
+ /**
+ * Verify that when authorized that the scripting variables is accessible
+ */
+ @RunWith(PaxExam.class)
+ @ExamReactorStrategy(PerClass.class)
+ public static class AuthorizedIT extends BaseIT {
+
+ @Test
+ public void testGetSlingBindingsVariablesListJsonWithAuthorizedUser() throws IOException, AuthenticationException {
+ // make GET request and verify it returned a 200 ok
+ HttpGet request = new HttpGet(String.format("http://localhost:%d/.SLING_availablebindings.json?extension=esp", httpPort()));
+ // pre-emptive basic authentication
+ request.addHeader(new BasicScheme().authenticate(new UsernamePasswordCredentials("admin", "admin"), request, null));
+
+ if (logger.isInfoEnabled()) {
+ logger.info("Executing GET Request to: {}", request.getURI());
+ }
+ try (CloseableHttpResponse response = httpClient.execute(request)) {
+ if (logger.isInfoEnabled()) {
+ logger.info("Response Content\r\n: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
+ }
+ // should have passed all security checks and returned the JSON payload
+ assertEquals(HttpServletResponse.SC_OK, response.getStatusLine().getStatusCode());
+ checkContentType(response, "application/json");
+ }
+ }
+ }
+
+ /**
+ * Verify that when not authorized that the scripting variables is not accessible
+ */
+ @RunWith(PaxExam.class)
+ @ExamReactorStrategy(PerClass.class)
+ public static class NotAuthorizedIT extends BaseIT {
+
+ @Configuration
+ @Override
+ public Option[] configuration() {
+ return composite(super.configuration())
+ .add(// testing - add a user to use to login and verify
+ factoryConfiguration("org.apache.sling.jcr.repoinit.RepositoryInitializer")
+ .put("scripts", new String[] {
+ "create user sling10147 with password sling10147"
+ })
+ .asOption()
+ )
+ .getOptions();
+ }
+
+ @Test
+ public void testGetSlingBindingsVariablesListJsonWithNotAuthorizedUser() throws IOException, AuthenticationException {
+ // make GET request and verify it is denied for user without sufficient rights
+ HttpGet request = new HttpGet(String.format("http://localhost:%d/.SLING_availablebindings.json?extension=esp", httpPort()));
+ // pre-emptive basic authentication
+ request.addHeader(new BasicScheme().authenticate(new UsernamePasswordCredentials("sling10147", "sling10147"), request, null));
+
+ if (logger.isInfoEnabled()) {
+ logger.info("Executing GET Request to: {}", request.getURI());
+ }
+ try (CloseableHttpResponse response = httpClient.execute(request)) {
+ if (logger.isInfoEnabled()) {
+ logger.info("Response Content\r\n: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
+ }
+ // should have been denied access by WebConsoleSecurityProvider2 service access not granted to the user
+ assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatusLine().getStatusCode());
+ checkContentType(response, "text/html");
+ }
+ }
+
+ @Test
+ public void testGetSlingBindingsVariablesListJsonWithAnonymousUser() throws IOException, AuthenticationException {
+ // make GET request and verify it returned a forms login page challenge
+ HttpGet request = new HttpGet(String.format("http://localhost:%d/.SLING_availablebindings.json?extension=esp", httpPort()));
+
+ if (logger.isInfoEnabled()) {
+ logger.info("Executing GET Request to: {}", request.getURI());
+ }
+ try (CloseableHttpResponse response = httpClient.execute(request)) {
+ String content = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
+ if (logger.isInfoEnabled()) {
+ logger.info("Response Content\r\n: {}", content);
+ }
+ // should have been challenged for authentication by the WebConsoleSecurity2#authenitcate call
+ assertEquals(HttpServletResponse.SC_OK, response.getStatusLine().getStatusCode());
+ checkContentType(response, "text/html");
+ assertTrue("Expected forms based login page", content.contains("Login to Apache Sling"));
+ }
+ }
+ }
+
+ /**
+ * Verify that when no WebConsoleSecurityProvider2 service is available that
+ * the scripting variables is not accessible
+ */
+ @RunWith(PaxExam.class)
+ @ExamReactorStrategy(PerClass.class)
+ public static class HasNoWebConsoleSecurityProvider2IT extends BaseIT {
+
+ @Configuration
+ @Override
+ public Option[] configuration() {
+ // remove the security provider bundle from the configuration
+ final Option webconsolesecurityprovider = mavenBundle().groupId("org.apache.sling").artifactId("org.apache.sling.extensions.webconsolesecurityprovider").versionAsInProject();
+ return composite(super.configuration())
+ .remove(webconsolesecurityprovider)
+ .getOptions();
+ }
+
+ @Test
+ public void testGetSlingBindingsVariablesListJsonWithoutWebConsoleSecurityProvider2() throws IOException, AuthenticationException {
+ // make GET request and verify it returned a 403 error
+ HttpGet request = new HttpGet(String.format("http://localhost:%d/.SLING_availablebindings.json?extension=esp", httpPort()));
+ // pre-emptive basic authentication
+ request.addHeader(new BasicScheme().authenticate(new UsernamePasswordCredentials("admin", "admin"), request, null));
+
+ if (logger.isInfoEnabled()) {
+ logger.info("Executing GET Request to: {}", request.getURI());
+ }
+ try (CloseableHttpResponse response = httpClient.execute(request)) {
+ if (logger.isInfoEnabled()) {
+ logger.info("Response Content\r\n: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
+ }
+ // should have failed fast due to the missing WebConsoleSecurityProvider2 service
+ assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatusLine().getStatusCode());
+ checkContentType(response, "text/html");
+ }
+ }
+
+ }
+
+}