diff --git a/jspwiki-main/src/main/java/org/apache/wiki/plugin/SampleAjaxPlugin.java b/jspwiki-main/src/main/java/org/apache/wiki/plugin/SampleAjaxPlugin.java index c439cdaa82..f5832eb65b 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/plugin/SampleAjaxPlugin.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/plugin/SampleAjaxPlugin.java @@ -22,6 +22,7 @@ Licensed to the Apache Software Foundation (ASF) under one import org.apache.wiki.api.core.Context; import org.apache.wiki.api.exceptions.PluginException; import org.apache.wiki.api.plugin.Plugin; +import org.apache.wiki.util.TextUtil; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -46,7 +47,7 @@ public String execute( final Context context, final Map params) var url = "/" + SERVLET_MAPPING + "/ajaxAction"; - var ajaxParams = params.get("params"); + var ajaxParams = jsStringEscape( params.get("params") ); var js = String.format("$('result%s').value='Loading…';"+ "var token = Wiki.CsrfProtection;"+ @@ -60,6 +61,31 @@ public String execute( final Context context, final Map params) return String.format("
Press Me
", js, id); } + /** + * Escapes a value for safe embedding inside a single-quoted javascript string that itself + * lives inside a single-quoted HTML event-handler attribute: every character other than an + * ASCII letter or digit is emitted as a javascript unicode escape, so the result can + * terminate neither the enclosing javascript string nor the enclosing attribute. + * + * @param s the raw parameter value, may be {@code null}. + * @return the escaped value, or an empty string if {@code s} was {@code null}. + */ + static String jsStringEscape( final String s ) { + if( s == null ) { + return ""; + } + final StringBuilder sb = new StringBuilder( s.length() ); + for( int i = 0; i < s.length(); i++ ) { + final char c = s.charAt( i ); + if( ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) || ( c >= '0' && c <= '9' ) ) { + sb.append( c ); + } else { + sb.append( String.format( "\\u%04x", ( int )c ) ); + } + } + return sb.toString(); + } + @Override public String getServletMapping() { return SERVLET_MAPPING; @@ -72,7 +98,8 @@ public void service( final HttpServletRequest request, final HttpServletResponse Thread.sleep( 5000 ); // Wait 5 seconds } catch( final Exception e ) { } - response.getWriter().print( "You called! actionName=" + actionName + " params=" + params ); + response.getWriter().print( "You called! actionName=" + TextUtil.replaceEntities( actionName ) + + " params=" + TextUtil.replaceEntities( String.valueOf( params ) ) ); } } diff --git a/jspwiki-main/src/test/java/org/apache/wiki/plugin/SampleAjaxPluginTest.java b/jspwiki-main/src/test/java/org/apache/wiki/plugin/SampleAjaxPluginTest.java new file mode 100644 index 0000000000..cda189891d --- /dev/null +++ b/jspwiki-main/src/test/java/org/apache/wiki/plugin/SampleAjaxPluginTest.java @@ -0,0 +1,44 @@ +/* + 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.wiki.plugin; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +public class SampleAjaxPluginTest { + + @Test + public void testParamsCannotBreakOutOfOnclickAttribute() throws Exception { + final SampleAjaxPlugin plugin = new SampleAjaxPlugin(); + final String payload = "\'>"; + final String result = plugin.execute( null, Map.of( "params", payload ) ); + Assertions.assertFalse( result.contains( payload ), "raw payload must not reach the HTML output" ); + Assertions.assertFalse( result.contains( "