This is an automated email from the ASF dual-hosted git repository.
benweidig pushed a commit to branch javax
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
The following commit(s) were added to refs/heads/javax by this push:
new c73f20418 TAP5-2835: ContextAssetProtectionRule chain added, don't
serve source maps in prod
c73f20418 is described below
commit c73f20418c15fc7d5ac1c487e94dab0293a98dcb
Author: Ben Weidig <[email protected]>
AuthorDate: Sat Jul 18 11:13:28 2026 +0200
TAP5-2835: ContextAssetProtectionRule chain added, don't serve source maps
in prod
---
.../assets/ContextAssetRequestHandler.java | 15 ++-
.../org/apache/tapestry5/modules/AssetsModule.java | 44 ++++++++-
.../services/ContextAssetProtectionRule.java | 34 +++++++
.../assets/ContextAssetRequestHandlerTest.java | 104 ++++++++++++++++-----
.../valid-asset.js | 1 +
.../valid-asset.js.map | 1 +
6 files changed, 169 insertions(+), 30 deletions(-)
diff --git
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandler.java
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandler.java
index fdb2f402c..1ba8e007f 100644
---
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandler.java
+++
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandler.java
@@ -1,4 +1,4 @@
-// Copyright 2010, 2013 The Apache Software Foundation
+// Copyright 2010, 2013, 2026 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ import org.apache.tapestry5.commons.Resource;
import org.apache.tapestry5.http.services.Request;
import org.apache.tapestry5.http.services.Response;
import org.apache.tapestry5.internal.services.ResourceStreamer;
+import org.apache.tapestry5.services.ContextAssetProtectionRule;
import org.apache.tapestry5.services.assets.AssetRequestHandler;
import java.io.IOException;
@@ -35,12 +36,16 @@ public class ContextAssetRequestHandler implements
AssetRequestHandler
private final Resource rootContextResource;
+ private final ContextAssetProtectionRule contextAssetProtectionRule;
+
private final Pattern illegal =
Pattern.compile("^([\\\\/]*((web|meta)-inf.*)|(.*\\.tml$))",
Pattern.CASE_INSENSITIVE);
- public ContextAssetRequestHandler(ResourceStreamer resourceStreamer,
Resource rootContextResource)
+ public ContextAssetRequestHandler(ResourceStreamer resourceStreamer,
Resource rootContextResource,
+ ContextAssetProtectionRule
contextAssetProtectionRule)
{
this.resourceStreamer = resourceStreamer;
this.rootContextResource = rootContextResource;
+ this.contextAssetProtectionRule = contextAssetProtectionRule;
}
public boolean handleAssetRequest(Request request, Response response,
String extraPath) throws IOException
@@ -52,6 +57,12 @@ public class ContextAssetRequestHandler implements
AssetRequestHandler
return false;
}
+ // TAP5-2835: Allow custom protection rules mirroring
ClasspathAssetProtectionRule
+ if (contextAssetProtectionRule.block(path.resourcePath))
+ {
+ return false;
+ }
+
return path.stream(rootContextResource.forFile(path.resourcePath));
}
diff --git
a/tapestry-core/src/main/java/org/apache/tapestry5/modules/AssetsModule.java
b/tapestry-core/src/main/java/org/apache/tapestry5/modules/AssetsModule.java
index e634285c8..dc414c448 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/AssetsModule.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/AssetsModule.java
@@ -76,6 +76,7 @@ import
org.apache.tapestry5.services.ClasspathAssetAliasManager;
import org.apache.tapestry5.services.ClasspathAssetProtectionRule;
import org.apache.tapestry5.services.ClasspathProvider;
import org.apache.tapestry5.services.ComponentClassResolver;
+import org.apache.tapestry5.services.ContextAssetProtectionRule;
import org.apache.tapestry5.services.ContextProvider;
import org.apache.tapestry5.services.Core;
import org.apache.tapestry5.services.assets.AssetChecksumGenerator;
@@ -331,7 +332,8 @@ public class AssetsModule
ClasspathAssetAliasManager classpathAssetAliasManager,
ResourceStreamer
streamer,
AssetSource assetSource,
-
ClasspathAssetProtectionRule classpathAssetProtectionRule)
+
ClasspathAssetProtectionRule classpathAssetProtectionRule,
+
ContextAssetProtectionRule contextAssetProtectionRule)
{
Map<String, String> mappings =
classpathAssetAliasManager.getMappings();
@@ -343,7 +345,7 @@ public class AssetsModule
}
configuration.add(RequestConstants.CONTEXT_FOLDER,
- new ContextAssetRequestHandler(streamer,
contextAssetFactory.getRootResource()));
+ new ContextAssetRequestHandler(streamer,
contextAssetFactory.getRootResource(), contextAssetProtectionRule));
configuration.add(RequestConstants.STACK_FOLDER,
stackAssetRequestHandler);
@@ -421,7 +423,9 @@ public class AssetsModule
}
public static void contributeClasspathAssetProtectionRule(
- OrderedConfiguration<ClasspathAssetProtectionRule> configuration)
+ OrderedConfiguration<ClasspathAssetProtectionRule> configuration,
+ @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE)
+ boolean productionMode)
{
ClasspathAssetProtectionRule classFileRule = (s) ->
s.toLowerCase().endsWith(".class");
configuration.add("ClassFile", classFileRule);
@@ -433,9 +437,39 @@ public class AssetsModule
configuration.add("TemplateFile", tmlFileRule);
ClasspathAssetProtectionRule folderRule = (s) -> isFolderToBlock(s);
configuration.add("Folder", folderRule);
+
+ // TAP5-2835: Don't serve source maps in production
+ if (productionMode)
+ {
+ ClasspathAssetProtectionRule mapFileRule = (s) ->
s.toLowerCase().endsWith(".map");
+ configuration.add("MapFile", mapFileRule);
+ }
}
-
- final private static boolean isFolderToBlock(String path)
+
+ @Primary
+ public static ContextAssetProtectionRule buildContextAssetProtectionRule(
+ List<ContextAssetProtectionRule> rules, ChainBuilder chainBuilder)
+ {
+ return chainBuilder.build(ContextAssetProtectionRule.class, rules);
+ }
+
+ @Contribute(ContextAssetProtectionRule.class)
+ public static void contributeContextAssetProtectionRule(
+ OrderedConfiguration<ContextAssetProtectionRule> configuration,
+
+ @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE)
+ boolean productionMode)
+ {
+ // Source maps expose original, unminified source; don't let them leak
out in production.
+ // Applications that want source maps served in production can
override this contribution.
+ if (productionMode)
+ {
+ ContextAssetProtectionRule mapFileRule = (s) ->
s.toLowerCase().endsWith(".map");
+ configuration.add("MapFile", mapFileRule);
+ }
+ }
+
+ final private static boolean isFolderToBlock(String path)
{
path = path.replace('\\', '/');
final int lastIndex = path.lastIndexOf('/');
diff --git
a/tapestry-core/src/main/java/org/apache/tapestry5/services/ContextAssetProtectionRule.java
b/tapestry-core/src/main/java/org/apache/tapestry5/services/ContextAssetProtectionRule.java
new file mode 100644
index 000000000..a977740e1
--- /dev/null
+++
b/tapestry-core/src/main/java/org/apache/tapestry5/services/ContextAssetProtectionRule.java
@@ -0,0 +1,34 @@
+// Copyright 2026 The Apache Software Foundation
+//
+// Licensed 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.tapestry5.services;
+
+import org.apache.tapestry5.ioc.annotations.UsesOrderedConfiguration;
+
+/**
+ * Chain-of-responsibility service which defines rules for blocking access to
context resources
+ * based on their paths. Access is blocked if any rule says it should be
blocked.
+ *
+ * @see ClasspathAssetProtectionRule
+ * @since 5.10.0
+ */
+@UsesOrderedConfiguration(ContextAssetProtectionRule.class)
+public interface ContextAssetProtectionRule
+{
+ /**
+ * Tells whether the access to the resource with this path should be
blocked or not.
+ * If this rule doesn't concern the given path, it should return false.
+ */
+ boolean block(String path);
+}
diff --git
a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandlerTest.java
b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandlerTest.java
index b80c078ca..8b9dc54b8 100644
---
a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandlerTest.java
+++
b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandlerTest.java
@@ -1,4 +1,4 @@
-// Copyright 2010, 2013 The Apache Software Foundation
+// Copyright 2010, 2013, 2026 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,36 +14,94 @@
package org.apache.tapestry5.internal.services.assets;
-import org.apache.tapestry5.test.ioc.TestBase;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.tapestry5.commons.Resource;
+import org.apache.tapestry5.internal.services.ResourceStreamer;
+import org.apache.tapestry5.ioc.internal.util.ClasspathResource;
+import org.apache.tapestry5.services.ContextAssetProtectionRule;
+import org.apache.tapestry5.services.assets.StreamableResource;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import java.io.IOException;
+import java.util.Set;
-public class ContextAssetRequestHandlerTest extends TestBase
+class ContextAssetRequestHandlerTest
{
- @DataProvider
- public Object[][] invalid_paths()
+ private static final ContextAssetProtectionRule NEVER_BLOCK = (path) ->
false;
+
+ private static final ContextAssetProtectionRule BLOCK_MAP_FILES = (path)
-> path.toLowerCase().endsWith(".map");
+
+ private static final Resource CONTEXT_ROOT = new ClasspathResource("");
+
+ private static final ResourceStreamer ALWAYS_STREAM = new
ResourceStreamer()
{
- return new Object[][]
- {
- {"web-Inf/classes/hibernate.cfg.xml"},
- {"Meta-Inf/MANIFEST.mf"},
- {"Index.tml"},
- {"folder/FolderIndex.TML"},
- {"\\WEB-INF/something.jpg"},
- {"\\//WEB-INF/something.jpg"},
- {"//WEB-INF/something.jpg"},
- {"//\\\\WEB-INF/something.jpg"}
- };
+ @Override
+ public boolean streamResource(Resource resource, String
providedChecksum, Set<Options> options)
+ {
+ return true;
+ }
+
+ @Override
+ public boolean streamResource(StreamableResource resource, String
providedChecksum, Set<Options> options)
+ {
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "web-Inf/classes/hibernate.cfg.xml",
+ "Meta-Inf/MANIFEST.mf",
+ "Index.tml",
+ "folder/FolderIndex.TML",
+ "\\WEB-INF/something.jpg",
+ "\\//WEB-INF/something.jpg",
+ "//WEB-INF/something.jpg",
+ "//\\\\WEB-INF/something.jpg"
+ })
+ void ensureAssetsAreRejected(String path) throws IOException
+ {
+ // ARRANGE
+ ContextAssetRequestHandler handler = new
ContextAssetRequestHandler(null, null, NEVER_BLOCK);
+
+ // ACT
+ boolean providedResponse = handler.handleAssetRequest(null, null,
"fake-checksum/" + path);
+
+ // ASSERT
+ assertFalse(providedResponse, "Handler should return false for invalid
path.");
}
- @Test(dataProvider = "invalid_paths")
- public void ensure_assets_are_rejected(String path) throws IOException
+ @Test
+ void contextAssetProtectionRuleIsConsulted() throws IOException
{
- ContextAssetRequestHandler handler = new
ContextAssetRequestHandler(null, null);
+ // ARRANGE
+ ContextAssetRequestHandler handler = new
ContextAssetRequestHandler(null, null, BLOCK_MAP_FILES);
+
+ // ACT
+ boolean providedResponse = handler.handleAssetRequest(null, null,
"fake-checksum/app.js.map");
+
+ // ASSERT
+ assertFalse(providedResponse, "Handler should return false when the
protection rule blocks the path.");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "context-asset-request-handler-test/valid-asset.js",
+ "context-asset-request-handler-test/valid-asset.js.map"
+ })
+ void validAssetsAreStreamedWhenNotBlocked(String path) throws IOException
+ {
+ // ARRANGE
+ ContextAssetRequestHandler handler = new
ContextAssetRequestHandler(ALWAYS_STREAM, CONTEXT_ROOT, NEVER_BLOCK);
+
+ // ACT
+ boolean providedResponse = handler.handleAssetRequest(null, null,
"fake-checksum/" + path);
- assertFalse(handler.handleAssetRequest(null, null, "fake-checksum/" +
path),
- "Handler should return false for invalid path.");
+ // ASSERT
+ assertTrue(providedResponse, "Handler should return true for a valid,
existing asset that no rule blocks.");
}
}
diff --git
a/tapestry-core/src/test/resources/context-asset-request-handler-test/valid-asset.js
b/tapestry-core/src/test/resources/context-asset-request-handler-test/valid-asset.js
new file mode 100644
index 000000000..4a0efb4a5
--- /dev/null
+++
b/tapestry-core/src/test/resources/context-asset-request-handler-test/valid-asset.js
@@ -0,0 +1 @@
+// Fixture for ContextAssetRequestHandlerTest; content is irrelevant, only
existence matters.
diff --git
a/tapestry-core/src/test/resources/context-asset-request-handler-test/valid-asset.js.map
b/tapestry-core/src/test/resources/context-asset-request-handler-test/valid-asset.js.map
new file mode 100644
index 000000000..b5415a8b4
--- /dev/null
+++
b/tapestry-core/src/test/resources/context-asset-request-handler-test/valid-asset.js.map
@@ -0,0 +1 @@
+{ "version": 3, "sources": [], "mappings": "" }