From fc3a39d464b1303f0b7d01d0160f81cbbb80a98b Mon Sep 17 00:00:00 2001 From: James Moger Date: Sat, 6 Sep 2014 11:25:42 -0400 Subject: Create infrastructure for XSS sanitization --- src/main/java/com/gitblit/DaggerModule.java | 11 ++- src/main/java/com/gitblit/FederationClient.java | 5 +- src/main/java/com/gitblit/MigrateTickets.java | 5 +- src/main/java/com/gitblit/ReindexTickets.java | 5 +- .../java/com/gitblit/manager/GitblitManager.java | 6 ++ .../java/com/gitblit/manager/IRuntimeManager.java | 8 ++ .../java/com/gitblit/manager/RuntimeManager.java | 21 +++++- .../java/com/gitblit/utils/JSoupXssFilter.java | 87 ++++++++++++++++++++++ src/main/java/com/gitblit/utils/XssFilter.java | 64 ++++++++++++++++ .../java/com/gitblit/wicket/GitBlitWebApp.java | 12 +++ .../java/com/gitblit/wicket/GitblitWicketApp.java | 3 + .../gitblit/tests/AuthenticationManagerTest.java | 5 +- .../com/gitblit/tests/BranchTicketServiceTest.java | 6 +- .../com/gitblit/tests/FileTicketServiceTest.java | 6 +- .../gitblit/tests/HtpasswdAuthenticationTest.java | 8 +- .../com/gitblit/tests/LdapAuthenticationTest.java | 8 +- .../java/com/gitblit/tests/LuceneExecutorTest.java | 5 +- .../com/gitblit/tests/RedisTicketServiceTest.java | 6 +- .../gitblit/tests/RedmineAuthenticationTest.java | 8 +- .../com/gitblit/tests/mock/MockRuntimeManager.java | 7 ++ 20 files changed, 264 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/gitblit/utils/JSoupXssFilter.java create mode 100644 src/main/java/com/gitblit/utils/XssFilter.java (limited to 'src') diff --git a/src/main/java/com/gitblit/DaggerModule.java b/src/main/java/com/gitblit/DaggerModule.java index 6ad3fe63..dd7e1b2b 100644 --- a/src/main/java/com/gitblit/DaggerModule.java +++ b/src/main/java/com/gitblit/DaggerModule.java @@ -38,7 +38,9 @@ import com.gitblit.transport.ssh.FileKeyManager; import com.gitblit.transport.ssh.IPublicKeyManager; import com.gitblit.transport.ssh.MemoryKeyManager; import com.gitblit.transport.ssh.NullKeyManager; +import com.gitblit.utils.JSoupXssFilter; import com.gitblit.utils.StringUtils; +import com.gitblit.utils.XssFilter; import com.gitblit.wicket.GitBlitWebApp; import dagger.Module; @@ -54,6 +56,7 @@ import dagger.Provides; library = true, injects = { IStoredSettings.class, + XssFilter.class, // core managers IRuntimeManager.class, @@ -79,8 +82,12 @@ public class DaggerModule { return new FileSettings(); } - @Provides @Singleton IRuntimeManager provideRuntimeManager(IStoredSettings settings) { - return new RuntimeManager(settings); + @Provides @Singleton XssFilter provideXssFilter() { + return new JSoupXssFilter(); + } + + @Provides @Singleton IRuntimeManager provideRuntimeManager(IStoredSettings settings, XssFilter xssFilter) { + return new RuntimeManager(settings, xssFilter); } @Provides @Singleton IPluginManager providePluginManager(IRuntimeManager runtimeManager) { diff --git a/src/main/java/com/gitblit/FederationClient.java b/src/main/java/com/gitblit/FederationClient.java index 29cdefe6..079355ef 100644 --- a/src/main/java/com/gitblit/FederationClient.java +++ b/src/main/java/com/gitblit/FederationClient.java @@ -36,6 +36,8 @@ import com.gitblit.models.Mailing; import com.gitblit.service.FederationPullService; import com.gitblit.utils.FederationUtils; import com.gitblit.utils.StringUtils; +import com.gitblit.utils.XssFilter; +import com.gitblit.utils.XssFilter.AllowXssFilter; /** * Command-line client to pull federated Gitblit repositories. @@ -92,7 +94,8 @@ public class FederationClient { } // configure the Gitblit singleton for minimal, non-server operation - RuntimeManager runtime = new RuntimeManager(settings, baseFolder).start(); + XssFilter xssFilter = new AllowXssFilter(); + RuntimeManager runtime = new RuntimeManager(settings, xssFilter, baseFolder).start(); NoopNotificationManager notifications = new NoopNotificationManager().start(); UserManager users = new UserManager(runtime, null).start(); RepositoryManager repositories = new RepositoryManager(runtime, null, users).start(); diff --git a/src/main/java/com/gitblit/MigrateTickets.java b/src/main/java/com/gitblit/MigrateTickets.java index ad1c63ea..94284ee2 100644 --- a/src/main/java/com/gitblit/MigrateTickets.java +++ b/src/main/java/com/gitblit/MigrateTickets.java @@ -39,6 +39,8 @@ import com.gitblit.tickets.FileTicketService; import com.gitblit.tickets.ITicketService; import com.gitblit.tickets.RedisTicketService; import com.gitblit.utils.StringUtils; +import com.gitblit.utils.XssFilter; +import com.gitblit.utils.XssFilter.AllowXssFilter; /** * A command-line tool to move all tickets from one ticket service to another. @@ -134,7 +136,8 @@ public class MigrateTickets { settings.overrideSetting(Keys.web.activityCacheDays, 0); settings.overrideSetting(ITicketService.SETTING_UPDATE_DIFFSTATS, false); - IRuntimeManager runtimeManager = new RuntimeManager(settings, baseFolder).start(); + XssFilter xssFilter = new AllowXssFilter(); + IRuntimeManager runtimeManager = new RuntimeManager(settings, xssFilter, baseFolder).start(); IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, null, null).start(); String inputServiceName = settings.getString(Keys.tickets.service, BranchTicketService.class.getSimpleName()); diff --git a/src/main/java/com/gitblit/ReindexTickets.java b/src/main/java/com/gitblit/ReindexTickets.java index 5a614481..858436af 100644 --- a/src/main/java/com/gitblit/ReindexTickets.java +++ b/src/main/java/com/gitblit/ReindexTickets.java @@ -33,6 +33,8 @@ import com.gitblit.tickets.FileTicketService; import com.gitblit.tickets.ITicketService; import com.gitblit.tickets.RedisTicketService; import com.gitblit.utils.StringUtils; +import com.gitblit.utils.XssFilter; +import com.gitblit.utils.XssFilter.AllowXssFilter; /** * A command-line tool to reindex all tickets in all repositories when the @@ -126,7 +128,8 @@ public class ReindexTickets { settings.overrideSetting(Keys.git.enableMirroring, false); settings.overrideSetting(Keys.web.activityCacheDays, 0); - IRuntimeManager runtimeManager = new RuntimeManager(settings, baseFolder).start(); + XssFilter xssFilter = new AllowXssFilter(); + IRuntimeManager runtimeManager = new RuntimeManager(settings, xssFilter, baseFolder).start(); IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, null, null).start(); String serviceName = settings.getString(Keys.tickets.service, BranchTicketService.class.getSimpleName()); diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java index b9ae122f..2ed52d67 100644 --- a/src/main/java/com/gitblit/manager/GitblitManager.java +++ b/src/main/java/com/gitblit/manager/GitblitManager.java @@ -79,6 +79,7 @@ import com.gitblit.tickets.ITicketService; import com.gitblit.transport.ssh.IPublicKeyManager; import com.gitblit.transport.ssh.SshKey; import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.XssFilter; import com.gitblit.utils.HttpUtils; import com.gitblit.utils.JsonUtils; import com.gitblit.utils.ObjectCache; @@ -663,6 +664,11 @@ public class GitblitManager implements IGitblit { return runtimeManager.getStatus(); } + @Override + public XssFilter getXssFilter() { + return runtimeManager.getXssFilter(); + } + /* * NOTIFICATION MANAGER */ diff --git a/src/main/java/com/gitblit/manager/IRuntimeManager.java b/src/main/java/com/gitblit/manager/IRuntimeManager.java index b2d7a2b3..132534c3 100644 --- a/src/main/java/com/gitblit/manager/IRuntimeManager.java +++ b/src/main/java/com/gitblit/manager/IRuntimeManager.java @@ -24,6 +24,7 @@ import java.util.TimeZone; import com.gitblit.IStoredSettings; import com.gitblit.models.ServerSettings; import com.gitblit.models.ServerStatus; +import com.gitblit.utils.XssFilter; public interface IRuntimeManager extends IManager { @@ -151,4 +152,11 @@ public interface IRuntimeManager extends IManager { * @since 1.4.0 */ boolean updateSettings(Map updatedSettings); + + /** + * Returns the HTML sanitizer used to clean user content. + * + * @return the HTML sanitizer + */ + XssFilter getXssFilter(); } \ No newline at end of file diff --git a/src/main/java/com/gitblit/manager/RuntimeManager.java b/src/main/java/com/gitblit/manager/RuntimeManager.java index 9cdc64eb..219bf801 100644 --- a/src/main/java/com/gitblit/manager/RuntimeManager.java +++ b/src/main/java/com/gitblit/manager/RuntimeManager.java @@ -32,6 +32,7 @@ import com.gitblit.models.ServerSettings; import com.gitblit.models.ServerStatus; import com.gitblit.models.SettingModel; import com.gitblit.utils.StringUtils; +import com.gitblit.utils.XssFilter; public class RuntimeManager implements IRuntimeManager { @@ -39,6 +40,8 @@ public class RuntimeManager implements IRuntimeManager { private final IStoredSettings settings; + private final XssFilter xssFilter; + private final ServerStatus serverStatus; private final ServerSettings settingsModel; @@ -47,14 +50,15 @@ public class RuntimeManager implements IRuntimeManager { private TimeZone timezone; - public RuntimeManager(IStoredSettings settings) { - this(settings, null); + public RuntimeManager(IStoredSettings settings, XssFilter xssFilter) { + this(settings, xssFilter, null); } - public RuntimeManager(IStoredSettings settings, File baseFolder) { + public RuntimeManager(IStoredSettings settings, XssFilter xssFilter, File baseFolder) { this.settings = settings; this.settingsModel = new ServerSettings(); this.serverStatus = new ServerStatus(); + this.xssFilter = xssFilter; this.baseFolder = baseFolder == null ? new File("") : baseFolder; } @@ -262,4 +266,15 @@ public class RuntimeManager implements IRuntimeManager { serverStatus.heapFree = Runtime.getRuntime().freeMemory(); return serverStatus; } + + /** + * Returns the XSS filter. + * + * @return the XSS filter + */ + @Override + public XssFilter getXssFilter() { + return xssFilter; + } + } diff --git a/src/main/java/com/gitblit/utils/JSoupXssFilter.java b/src/main/java/com/gitblit/utils/JSoupXssFilter.java new file mode 100644 index 00000000..b07bcb9d --- /dev/null +++ b/src/main/java/com/gitblit/utils/JSoupXssFilter.java @@ -0,0 +1,87 @@ +/* + * Copyright 2014 gitblit.com. + * + * 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 com.gitblit.utils; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Cleaner; +import org.jsoup.safety.Whitelist; + +/** + * Implementation of an XSS filter based on JSoup. + * + * @author James Moger + * + */ +public class JSoupXssFilter implements XssFilter { + + private final Cleaner none; + + private final Cleaner relaxed; + + public JSoupXssFilter() { + none = new Cleaner(Whitelist.none()); + relaxed = new Cleaner(getRelaxedWhiteList()); + } + + @Override + public String none(String input) { + return clean(input, none); + } + + @Override + public String relaxed(String input) { + return clean(input, relaxed); + } + + protected String clean(String input, Cleaner cleaner) { + Document unsafe = Jsoup.parse(input); + Document safe = cleaner.clean(unsafe); + return safe.body().html(); + } + + /** + * Builds & returns a loose HTML whitelist similar to Github. + * + * https://github.com/github/markup/tree/master#html-sanitization + * @return a loose HTML whitelist + */ + protected Whitelist getRelaxedWhiteList() { + return new Whitelist() + .addTags( + "a", "b", "blockquote", "br", "caption", "cite", "code", "col", + "colgroup", "dd", "del", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "hr", + "i", "img", "ins", "kbd", "li", "ol", "p", "pre", "q", "samp", "small", "strike", "strong", + "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "tt", "u", + "ul", "var") + + .addAttributes("a", "href", "title") + .addAttributes("blockquote", "cite") + .addAttributes("col", "span", "width") + .addAttributes("colgroup", "span", "width") + .addAttributes("img", "align", "alt", "height", "src", "title", "width") + .addAttributes("ol", "start", "type") + .addAttributes("q", "cite") + .addAttributes("table", "summary", "width") + .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width") + .addAttributes("th", "abbr", "axis", "colspan", "rowspan", "scope", "width") + .addAttributes("ul", "type") + + .addEnforcedAttribute("a", "rel", "nofollow") + ; + } + +} diff --git a/src/main/java/com/gitblit/utils/XssFilter.java b/src/main/java/com/gitblit/utils/XssFilter.java new file mode 100644 index 00000000..20b51057 --- /dev/null +++ b/src/main/java/com/gitblit/utils/XssFilter.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014 gitblit.com. + * + * 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 com.gitblit.utils; + +/** + * Defines the contract for an XSS filter implementation. + * + * @author James Moger + * + */ +public interface XssFilter { + + /** + * Returns a filtered version of the input value that contains no html + * elements. + * + * @param input + * @return a plain text value + */ + String none(String input); + + /** + * Returns a filtered version of the input that contains structural html + * elements. + * + * @param input + * @return a filtered html value + */ + String relaxed(String input); + + /** + * A NOOP XSS filter. + * + * @author James Moger + * + */ + public class AllowXssFilter implements XssFilter { + + @Override + public String none(String input) { + return input; + } + + @Override + public String relaxed(String input) { + return input; + } + + } + +} diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java index f63ff3d9..6cf5f582 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java @@ -46,6 +46,7 @@ import com.gitblit.manager.IRuntimeManager; import com.gitblit.manager.IUserManager; import com.gitblit.tickets.ITicketService; import com.gitblit.transport.ssh.IPublicKeyManager; +import com.gitblit.utils.XssFilter; import com.gitblit.wicket.pages.ActivityPage; import com.gitblit.wicket.pages.BlamePage; import com.gitblit.wicket.pages.BlobDiffPage; @@ -100,6 +101,8 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp { private final IStoredSettings settings; + private final XssFilter xssFilter; + private final IRuntimeManager runtimeManager; private final IPluginManager pluginManager; @@ -134,6 +137,7 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp { super(); this.settings = runtimeManager.getSettings(); + this.xssFilter = runtimeManager.getXssFilter(); this.runtimeManager = runtimeManager; this.pluginManager = pluginManager; this.notificationManager = notificationManager; @@ -307,6 +311,14 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp { return settings; } + /* (non-Javadoc) + * @see com.gitblit.wicket.Webapp#xssFilter() + */ + @Override + public XssFilter xssFilter() { + return xssFilter; + } + /* (non-Javadoc) * @see com.gitblit.wicket.Webapp#isDebugMode() */ diff --git a/src/main/java/com/gitblit/wicket/GitblitWicketApp.java b/src/main/java/com/gitblit/wicket/GitblitWicketApp.java index a56e6996..8d3d598d 100644 --- a/src/main/java/com/gitblit/wicket/GitblitWicketApp.java +++ b/src/main/java/com/gitblit/wicket/GitblitWicketApp.java @@ -17,6 +17,7 @@ import com.gitblit.manager.IRuntimeManager; import com.gitblit.manager.IUserManager; import com.gitblit.tickets.ITicketService; import com.gitblit.transport.ssh.IPublicKeyManager; +import com.gitblit.utils.XssFilter; public interface GitblitWicketApp { @@ -30,6 +31,8 @@ public interface GitblitWicketApp { public abstract IStoredSettings settings(); + public abstract XssFilter xssFilter(); + /** * Is Gitblit running in debug mode? * diff --git a/src/test/java/com/gitblit/tests/AuthenticationManagerTest.java b/src/test/java/com/gitblit/tests/AuthenticationManagerTest.java index f1d2711e..0cdee6cb 100644 --- a/src/test/java/com/gitblit/tests/AuthenticationManagerTest.java +++ b/src/test/java/com/gitblit/tests/AuthenticationManagerTest.java @@ -26,6 +26,8 @@ import com.gitblit.manager.RuntimeManager; import com.gitblit.manager.UserManager; import com.gitblit.models.UserModel; import com.gitblit.tests.mock.MemorySettings; +import com.gitblit.utils.XssFilter; +import com.gitblit.utils.XssFilter.AllowXssFilter; /** * Class for testing local authentication. @@ -42,7 +44,8 @@ public class AuthenticationManagerTest extends GitblitUnitTest { } IAuthenticationManager newAuthenticationManager() { - RuntimeManager runtime = new RuntimeManager(getSettings(), GitBlitSuite.BASEFOLDER).start(); + XssFilter xssFilter = new AllowXssFilter(); + RuntimeManager runtime = new RuntimeManager(getSettings(), xssFilter, GitBlitSuite.BASEFOLDER).start(); users = new UserManager(runtime, null).start(); AuthenticationManager auth = new AuthenticationManager(runtime, users).start(); return auth; diff --git a/src/test/java/com/gitblit/tests/BranchTicketServiceTest.java b/src/test/java/com/gitblit/tests/BranchTicketServiceTest.java index cc404abf..0a5de196 100644 --- a/src/test/java/com/gitblit/tests/BranchTicketServiceTest.java +++ b/src/test/java/com/gitblit/tests/BranchTicketServiceTest.java @@ -29,6 +29,8 @@ import com.gitblit.manager.UserManager; import com.gitblit.models.RepositoryModel; import com.gitblit.tickets.BranchTicketService; import com.gitblit.tickets.ITicketService; +import com.gitblit.utils.XssFilter; +import com.gitblit.utils.XssFilter.AllowXssFilter; /** * Tests the branch ticket service. @@ -50,8 +52,8 @@ public class BranchTicketServiceTest extends TicketServiceTest { protected ITicketService getService(boolean deleteAll) throws Exception { IStoredSettings settings = getSettings(deleteAll); - - IRuntimeManager runtimeManager = new RuntimeManager(settings).start(); + XssFilter xssFilter = new AllowXssFilter(); + IRuntimeManager runtimeManager = new RuntimeManager(settings, xssFilter).start(); IPluginManager pluginManager = new PluginManager(runtimeManager).start(); INotificationManager notificationManager = new NotificationManager(settings).start(); IUserManager userManager = new UserManager(runtimeManager, pluginManager).start(); diff --git a/src/test/java/com/gitblit/tests/FileTicketServiceTest.java b/src/test/java/com/gitblit/tests/FileTicketServiceTest.java index 6ede042a..1fb2eed9 100644 --- a/src/test/java/com/gitblit/tests/FileTicketServiceTest.java +++ b/src/test/java/com/gitblit/tests/FileTicketServiceTest.java @@ -29,6 +29,8 @@ import com.gitblit.manager.UserManager; import com.gitblit.models.RepositoryModel; import com.gitblit.tickets.FileTicketService; import com.gitblit.tickets.ITicketService; +import com.gitblit.utils.XssFilter; +import com.gitblit.utils.XssFilter.AllowXssFilter; /** * Tests the file ticket service. @@ -49,8 +51,8 @@ public class FileTicketServiceTest extends TicketServiceTest { protected ITicketService getService(boolean deleteAll) throws Exception { IStoredSettings settings = getSettings(deleteAll); - - IRuntimeManager runtimeManager = new RuntimeManager(settings).start(); + XssFilter xssFilter = new AllowXssFilter(); + IRuntimeManager runtimeManager = new RuntimeManager(settings, xssFilter).start(); IPluginManager pluginManager = new PluginManager(runtimeManager).start(); INotificationManager notificationManager = new NotificationManager(settings).start(); IUserManager userManager = new UserManager(runtimeManager, pluginManager).start(); diff --git a/src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java b/src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java index f4e24d4e..e2bb764e 100644 --- a/src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java +++ b/src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java @@ -32,6 +32,8 @@ import com.gitblit.manager.RuntimeManager; import com.gitblit.manager.UserManager; import com.gitblit.models.UserModel; import com.gitblit.tests.mock.MemorySettings; +import com.gitblit.utils.XssFilter; +import com.gitblit.utils.XssFilter.AllowXssFilter; /** * Test the Htpasswd user service. @@ -74,7 +76,8 @@ public class HtpasswdAuthenticationTest extends GitblitUnitTest { } private HtpasswdAuthProvider newHtpasswdAuthentication(IStoredSettings settings) { - RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start(); + XssFilter xssFilter = new AllowXssFilter(); + RuntimeManager runtime = new RuntimeManager(settings, xssFilter, GitBlitSuite.BASEFOLDER).start(); UserManager users = new UserManager(runtime, null).start(); HtpasswdAuthProvider htpasswd = new HtpasswdAuthProvider(); htpasswd.setup(runtime, users); @@ -82,7 +85,8 @@ public class HtpasswdAuthenticationTest extends GitblitUnitTest { } private AuthenticationManager newAuthenticationManager(IStoredSettings settings) { - RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start(); + XssFilter xssFilter = new AllowXssFilter(); + RuntimeManager runtime = new RuntimeManager(settings, xssFilter, GitBlitSuite.BASEFOLDER).start(); UserManager users = new UserManager(runtime, null).start(); HtpasswdAuthProvider htpasswd = new HtpasswdAuthProvider(); htpasswd.setup(runtime, users); diff --git a/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java b/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java index 646f7e9f..7c84ecc2 100644 --- a/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java +++ b/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java @@ -39,6 +39,8 @@ import com.gitblit.manager.UserManager; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; import com.gitblit.tests.mock.MemorySettings; +import com.gitblit.utils.XssFilter; +import com.gitblit.utils.XssFilter.AllowXssFilter; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; @@ -96,7 +98,8 @@ public class LdapAuthenticationTest extends GitblitUnitTest { } private LdapAuthProvider newLdapAuthentication(IStoredSettings settings) { - RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start(); + XssFilter xssFilter = new AllowXssFilter(); + RuntimeManager runtime = new RuntimeManager(settings, xssFilter, GitBlitSuite.BASEFOLDER).start(); userManager = new UserManager(runtime, null).start(); LdapAuthProvider ldap = new LdapAuthProvider(); ldap.setup(runtime, userManager); @@ -104,7 +107,8 @@ public class LdapAuthenticationTest extends GitblitUnitTest { } private AuthenticationManager newAuthenticationManager(IStoredSettings settings) { - RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start(); + XssFilter xssFilter = new AllowXssFilter(); + RuntimeManager runtime = new RuntimeManager(settings, xssFilter, GitBlitSuite.BASEFOLDER).start(); AuthenticationManager auth = new AuthenticationManager(runtime, userManager); auth.addAuthenticationProvider(newLdapAuthentication(settings)); return auth; diff --git a/src/test/java/com/gitblit/tests/LuceneExecutorTest.java b/src/test/java/com/gitblit/tests/LuceneExecutorTest.java index 5c319e65..a8358b99 100644 --- a/src/test/java/com/gitblit/tests/LuceneExecutorTest.java +++ b/src/test/java/com/gitblit/tests/LuceneExecutorTest.java @@ -34,6 +34,8 @@ import com.gitblit.service.LuceneService; import com.gitblit.tests.mock.MemorySettings; import com.gitblit.utils.FileUtils; import com.gitblit.utils.JGitUtils; +import com.gitblit.utils.XssFilter; +import com.gitblit.utils.XssFilter.AllowXssFilter; /** * Tests Lucene indexing and querying. @@ -48,7 +50,8 @@ public class LuceneExecutorTest extends GitblitUnitTest { private LuceneService newLuceneExecutor() { MemorySettings settings = new MemorySettings(); settings.put(Keys.git.repositoriesFolder, GitBlitSuite.REPOSITORIES); - RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start(); + XssFilter xssFilter = new AllowXssFilter(); + RuntimeManager runtime = new RuntimeManager(settings, xssFilter, GitBlitSuite.BASEFOLDER).start(); UserManager users = new UserManager(runtime, null).start(); RepositoryManager repos = new RepositoryManager(runtime, null, users); return new LuceneService(settings, repos); diff --git a/src/test/java/com/gitblit/tests/RedisTicketServiceTest.java b/src/test/java/com/gitblit/tests/RedisTicketServiceTest.java index b782b449..48011ade 100644 --- a/src/test/java/com/gitblit/tests/RedisTicketServiceTest.java +++ b/src/test/java/com/gitblit/tests/RedisTicketServiceTest.java @@ -30,6 +30,8 @@ import com.gitblit.manager.UserManager; import com.gitblit.models.RepositoryModel; import com.gitblit.tickets.ITicketService; import com.gitblit.tickets.RedisTicketService; +import com.gitblit.utils.XssFilter; +import com.gitblit.utils.XssFilter.AllowXssFilter; /** * Tests the Redis ticket service. @@ -57,8 +59,8 @@ public class RedisTicketServiceTest extends TicketServiceTest { protected ITicketService getService(boolean deleteAll) throws Exception { IStoredSettings settings = getSettings(deleteAll); - - IRuntimeManager runtimeManager = new RuntimeManager(settings).start(); + XssFilter xssFilter = new AllowXssFilter(); + IRuntimeManager runtimeManager = new RuntimeManager(settings, xssFilter).start(); IPluginManager pluginManager = new PluginManager(runtimeManager).start(); INotificationManager notificationManager = new NotificationManager(settings).start(); IUserManager userManager = new UserManager(runtimeManager, pluginManager).start(); diff --git a/src/test/java/com/gitblit/tests/RedmineAuthenticationTest.java b/src/test/java/com/gitblit/tests/RedmineAuthenticationTest.java index 3b6b7bba..ad773b7a 100644 --- a/src/test/java/com/gitblit/tests/RedmineAuthenticationTest.java +++ b/src/test/java/com/gitblit/tests/RedmineAuthenticationTest.java @@ -13,6 +13,8 @@ import com.gitblit.manager.RuntimeManager; import com.gitblit.manager.UserManager; import com.gitblit.models.UserModel; import com.gitblit.tests.mock.MemorySettings; +import com.gitblit.utils.XssFilter; +import com.gitblit.utils.XssFilter.AllowXssFilter; public class RedmineAuthenticationTest extends GitblitUnitTest { @@ -25,7 +27,8 @@ public class RedmineAuthenticationTest extends GitblitUnitTest { } RedmineAuthProvider newRedmineAuthentication(IStoredSettings settings) { - RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start(); + XssFilter xssFilter = new AllowXssFilter(); + RuntimeManager runtime = new RuntimeManager(settings, xssFilter, GitBlitSuite.BASEFOLDER).start(); UserManager users = new UserManager(runtime, null).start(); RedmineAuthProvider redmine = new RedmineAuthProvider(); redmine.setup(runtime, users); @@ -37,7 +40,8 @@ public class RedmineAuthenticationTest extends GitblitUnitTest { } AuthenticationManager newAuthenticationManager() { - RuntimeManager runtime = new RuntimeManager(getSettings(), GitBlitSuite.BASEFOLDER).start(); + XssFilter xssFilter = new AllowXssFilter(); + RuntimeManager runtime = new RuntimeManager(getSettings(), xssFilter, GitBlitSuite.BASEFOLDER).start(); UserManager users = new UserManager(runtime, null).start(); RedmineAuthProvider redmine = new RedmineAuthProvider(); redmine.setup(runtime, users); diff --git a/src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java b/src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java index 54be539f..7b563622 100644 --- a/src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java +++ b/src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java @@ -28,6 +28,8 @@ import com.gitblit.manager.IRuntimeManager; import com.gitblit.models.ServerSettings; import com.gitblit.models.ServerStatus; import com.gitblit.models.SettingModel; +import com.gitblit.utils.XssFilter; +import com.gitblit.utils.XssFilter.AllowXssFilter; public class MockRuntimeManager implements IRuntimeManager { @@ -147,6 +149,11 @@ public class MockRuntimeManager implements IRuntimeManager { return settings; } + @Override + public XssFilter getXssFilter() { + return new AllowXssFilter(); + } + @Override public boolean updateSettings(Map updatedSettings) { return settings.saveSettings(updatedSettings); -- cgit v1.2.3 From dfaf1fc1f6d8214bcabb9a613d53d0f0dc45352c Mon Sep 17 00:00:00 2001 From: James Moger Date: Sat, 6 Sep 2014 11:27:04 -0400 Subject: XSS sanitize standard page url parameters --- .../java/com/gitblit/wicket/GitBlitWebApp.java | 2 +- .../wicket/GitblitParamUrlCodingStrategy.java | 408 +++++++++++---------- 2 files changed, 221 insertions(+), 189 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java index 6cf5f582..38dbf57d 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java @@ -255,7 +255,7 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp { if (!settings.getBoolean(Keys.web.mountParameters, true)) { parameters = new String[] {}; } - mount(new GitblitParamUrlCodingStrategy(settings, location, clazz, parameters)); + mount(new GitblitParamUrlCodingStrategy(settings, xssFilter, location, clazz, parameters)); // map the mount point to the cache control definition if (clazz.isAnnotationPresent(CacheControl.class)) { diff --git a/src/main/java/com/gitblit/wicket/GitblitParamUrlCodingStrategy.java b/src/main/java/com/gitblit/wicket/GitblitParamUrlCodingStrategy.java index c6583946..536f88f4 100644 --- a/src/main/java/com/gitblit/wicket/GitblitParamUrlCodingStrategy.java +++ b/src/main/java/com/gitblit/wicket/GitblitParamUrlCodingStrategy.java @@ -1,189 +1,221 @@ -/* - * Copyright 2011 gitblit.com. - * - * 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 com.gitblit.wicket; - -import java.text.MessageFormat; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.apache.wicket.IRequestTarget; -import org.apache.wicket.Page; -import org.apache.wicket.request.RequestParameters; -import org.apache.wicket.request.target.coding.MixedParamUrlCodingStrategy; -import org.apache.wicket.util.string.AppendingStringBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.IStoredSettings; -import com.gitblit.Keys; - -/** - * Simple subclass of mixed parameter url coding strategy that works around the - * encoded forward-slash issue that is present in some servlet containers. - * - * https://issues.apache.org/jira/browse/WICKET-1303 - * http://tomcat.apache.org/security-6.html - * - * @author James Moger - * - */ -public class GitblitParamUrlCodingStrategy extends MixedParamUrlCodingStrategy { - - private final String[] parameterNames; - - private Logger logger = LoggerFactory.getLogger(GitblitParamUrlCodingStrategy.class); - - private IStoredSettings settings; - - /** - * Construct. - * - * @param - * @param mountPath - * mount path (not empty) - * @param bookmarkablePageClass - * class of mounted page (not null) - * @param parameterNames - * the parameter names (not null) - */ - public GitblitParamUrlCodingStrategy( - IStoredSettings settings, - String mountPath, - Class bookmarkablePageClass, String[] parameterNames) { - - super(mountPath, bookmarkablePageClass, parameterNames); - this.parameterNames = parameterNames; - this.settings = settings; - } - - /** - * Url encodes a string that is mean for a URL path (e.g., between slashes) - * - * @param string - * string to be encoded - * @return encoded string - */ - @Override - protected String urlEncodePathComponent(String string) { - char altChar = settings.getChar(Keys.web.forwardSlashCharacter, '/'); - if (altChar != '/') { - string = string.replace('/', altChar); - } - return super.urlEncodePathComponent(string); - } - - /** - * Returns a decoded value of the given value (taken from a URL path - * section) - * - * @param value - * @return Decodes the value - */ - @Override - protected String urlDecodePathComponent(String value) { - char altChar = settings.getChar(Keys.web.forwardSlashCharacter, '/'); - if (altChar != '/') { - value = value.replace(altChar, '/'); - } - return super.urlDecodePathComponent(value); - } - - /** - * Gets the decoded request target. - * - * @param requestParameters - * the request parameters - * @return the decoded request target - */ - @Override - public IRequestTarget decode(RequestParameters requestParameters) { - final String parametersFragment = requestParameters.getPath().substring( - getMountPath().length()); - logger.debug(MessageFormat - .format("REQ: {0} PARAMS {1}", getMountPath(), parametersFragment)); - - return super.decode(requestParameters); - } - - /** - * @see org.apache.wicket.request.target.coding.AbstractRequestTargetUrlCodingStrategy#appendParameters(org.apache.wicket.util.string.AppendingStringBuffer, - * java.util.Map) - */ - @Override - protected void appendParameters(AppendingStringBuffer url, Map parameters) - { - if (!url.endsWith("/")) - { - url.append("/"); - } - - Set parameterNamesToAdd = new HashSet(parameters.keySet()); - - // Find index of last specified parameter - boolean foundParameter = false; - int lastSpecifiedParameter = parameterNames.length; - while (lastSpecifiedParameter != 0 && !foundParameter) - { - foundParameter = parameters.containsKey(parameterNames[--lastSpecifiedParameter]); - } - - if (foundParameter) - { - for (int i = 0; i <= lastSpecifiedParameter; i++) - { - String parameterName = parameterNames[i]; - final Object param = parameters.get(parameterName); - String value = param instanceof String[] ? ((String[])param)[0] : ((param == null) - ? null : param.toString()); - if (value == null) - { - value = ""; - } - if (!url.endsWith("/")) - { - url.append("/"); - } - url.append(urlEncodePathComponent(value)); - parameterNamesToAdd.remove(parameterName); - } - } - - if (!parameterNamesToAdd.isEmpty()) - { - boolean first = true; - for (String parameterName : parameterNamesToAdd) - { - final Object param = parameters.get(parameterName); - if (param instanceof String[]) { - String [] values = (String[]) param; - for (String value : values) { - url.append(first ? '?' : '&'); - url.append(urlEncodeQueryComponent(parameterName)).append("=").append( - urlEncodeQueryComponent(value)); - first = false; - } - } else { - url.append(first ? '?' : '&'); - String value = String.valueOf(param); - url.append(urlEncodeQueryComponent(parameterName)).append("=").append( - urlEncodeQueryComponent(value)); - } - first = false; - } - } - } +/* + * Copyright 2011 gitblit.com. + * + * 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 com.gitblit.wicket; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.wicket.IRequestTarget; +import org.apache.wicket.Page; +import org.apache.wicket.protocol.http.request.WebRequestCodingStrategy; +import org.apache.wicket.request.RequestParameters; +import org.apache.wicket.request.target.coding.MixedParamUrlCodingStrategy; +import org.apache.wicket.util.string.AppendingStringBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.IStoredSettings; +import com.gitblit.Keys; +import com.gitblit.utils.XssFilter; + +/** + * Simple subclass of mixed parameter url coding strategy that works around the + * encoded forward-slash issue that is present in some servlet containers. + * + * https://issues.apache.org/jira/browse/WICKET-1303 + * http://tomcat.apache.org/security-6.html + * + * @author James Moger + * + */ +public class GitblitParamUrlCodingStrategy extends MixedParamUrlCodingStrategy { + + private final String[] parameterNames; + + private Logger logger = LoggerFactory.getLogger(GitblitParamUrlCodingStrategy.class); + + private IStoredSettings settings; + + private XssFilter xssFilter; + + /** + * Construct. + * + * @param + * @param mountPath + * mount path (not empty) + * @param bookmarkablePageClass + * class of mounted page (not null) + * @param parameterNames + * the parameter names (not null) + */ + public GitblitParamUrlCodingStrategy( + IStoredSettings settings, + XssFilter xssFilter, + String mountPath, + Class bookmarkablePageClass, String[] parameterNames) { + + super(mountPath, bookmarkablePageClass, parameterNames); + this.parameterNames = parameterNames; + this.settings = settings; + this.xssFilter = xssFilter; + } + + /** + * Url encodes a string that is mean for a URL path (e.g., between slashes) + * + * @param string + * string to be encoded + * @return encoded string + */ + @Override + protected String urlEncodePathComponent(String string) { + char altChar = settings.getChar(Keys.web.forwardSlashCharacter, '/'); + if (altChar != '/') { + string = string.replace('/', altChar); + } + return super.urlEncodePathComponent(string); + } + + /** + * Returns a decoded value of the given value (taken from a URL path + * section) + * + * @param value + * @return Decodes the value + */ + @Override + protected String urlDecodePathComponent(String value) { + char altChar = settings.getChar(Keys.web.forwardSlashCharacter, '/'); + if (altChar != '/') { + value = value.replace(altChar, '/'); + } + return super.urlDecodePathComponent(value); + } + + /** + * Gets the decoded request target. + * + * @param requestParameters + * the request parameters + * @return the decoded request target + */ + @Override + public IRequestTarget decode(RequestParameters requestParameters) { + Map parameterMap = (Map) requestParameters.getParameters(); + for (Map.Entry entry : parameterMap.entrySet()) { + String parameter = entry.getKey(); + if (parameter.startsWith(WebRequestCodingStrategy.NAME_SPACE)) { + // ignore Wicket parameters + continue; + } + + // sanitize Gitblit request parameters + Object o = entry.getValue(); + if (o instanceof String) { + String value = o.toString(); + String safeValue = xssFilter.none(value); + if (!value.equals(safeValue)) { + logger.warn("XSS filter triggered on {} URL parameter: {}={}", + getMountPath(), parameter, value); + parameterMap.put(parameter, safeValue); + } + } else if (o instanceof String[]) { + String[] values = (String[]) o; + for (int i = 0; i < values.length; i++) { + String value = values[i].toString(); + String safeValue = xssFilter.none(value); + if (!value.equals(safeValue)) { + logger.warn("XSS filter triggered on {} URL parameter: {}={}", + getMountPath(), parameter, value); + values[i] = safeValue; + } + } + } + } + + return super.decode(requestParameters); + } + + /** + * @see org.apache.wicket.request.target.coding.AbstractRequestTargetUrlCodingStrategy#appendParameters(org.apache.wicket.util.string.AppendingStringBuffer, + * java.util.Map) + */ + @Override + protected void appendParameters(AppendingStringBuffer url, Map parameters) + { + if (!url.endsWith("/")) + { + url.append("/"); + } + + Set parameterNamesToAdd = new HashSet(parameters.keySet()); + + // Find index of last specified parameter + boolean foundParameter = false; + int lastSpecifiedParameter = parameterNames.length; + while (lastSpecifiedParameter != 0 && !foundParameter) + { + foundParameter = parameters.containsKey(parameterNames[--lastSpecifiedParameter]); + } + + if (foundParameter) + { + for (int i = 0; i <= lastSpecifiedParameter; i++) + { + String parameterName = parameterNames[i]; + final Object param = parameters.get(parameterName); + String value = param instanceof String[] ? ((String[])param)[0] : ((param == null) + ? null : param.toString()); + if (value == null) + { + value = ""; + } + if (!url.endsWith("/")) + { + url.append("/"); + } + url.append(urlEncodePathComponent(value)); + parameterNamesToAdd.remove(parameterName); + } + } + + if (!parameterNamesToAdd.isEmpty()) + { + boolean first = true; + for (String parameterName : parameterNamesToAdd) + { + final Object param = parameters.get(parameterName); + if (param instanceof String[]) { + String [] values = (String[]) param; + for (String value : values) { + url.append(first ? '?' : '&'); + url.append(urlEncodeQueryComponent(parameterName)).append("=").append( + urlEncodeQueryComponent(value)); + first = false; + } + } else { + url.append(first ? '?' : '&'); + String value = String.valueOf(param); + url.append(urlEncodeQueryComponent(parameterName)).append("=").append( + urlEncodeQueryComponent(value)); + } + first = false; + } + } + } } \ No newline at end of file -- cgit v1.2.3 From 209dbdd49a89d6e3cebf61e860c779a1d8561dd9 Mon Sep 17 00:00:00 2001 From: James Moger Date: Sat, 6 Sep 2014 13:14:38 -0400 Subject: Implement a SafeTextModel and use that for fields vulnerable to XSS --- .../java/com/gitblit/wicket/SafeTextModel.java | 96 ++++++++++++++++++++++ .../com/gitblit/wicket/pages/EditTicketPage.java | 8 +- .../com/gitblit/wicket/pages/NewTicketPage.java | 8 +- .../com/gitblit/wicket/panels/CommentPanel.java | 5 +- .../gitblit/wicket/panels/MarkdownTextArea.java | 6 +- 5 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/gitblit/wicket/SafeTextModel.java (limited to 'src') diff --git a/src/main/java/com/gitblit/wicket/SafeTextModel.java b/src/main/java/com/gitblit/wicket/SafeTextModel.java new file mode 100644 index 00000000..aef7e97a --- /dev/null +++ b/src/main/java/com/gitblit/wicket/SafeTextModel.java @@ -0,0 +1,96 @@ +package com.gitblit.wicket; + +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; +import org.apache.wicket.util.lang.Objects; +import org.parboiled.common.StringUtils; +import org.slf4j.LoggerFactory; + +public class SafeTextModel implements IModel { + + private static final long serialVersionUID = 1L; + + public enum Mode { + relaxed, none + } + + private final Mode mode; + + private String value; + + public static SafeTextModel none() { + return new SafeTextModel(Mode.none); + } + + public static SafeTextModel none(String value) { + return new SafeTextModel(Mode.none); + } + + public static SafeTextModel relaxed() { + return new SafeTextModel(Mode.relaxed); + } + + public static SafeTextModel relaxed(String value) { + return new SafeTextModel(Mode.relaxed); + } + + public SafeTextModel(Mode mode) { + this.mode = mode; + } + + public SafeTextModel(String value, Mode mode) { + this.value = value; + this.mode = mode; + } + + @Override + public void detach() { + } + + @Override + public String getObject() { + if (StringUtils.isEmpty(value)) { + return value; + } + String safeValue; + switch (mode) { + case none: + safeValue = GitBlitWebApp.get().xssFilter().none(value); + break; + default: + safeValue = GitBlitWebApp.get().xssFilter().relaxed(value); + break; + } + if (!value.equals(safeValue)) { + LoggerFactory.getLogger(getClass()).warn("XSS filter trigggered on suspicious form field value {}", + value); + } + return safeValue; + } + + @Override + public void setObject(String input) { + this.value = input; + } + + @Override + public int hashCode() + { + return Objects.hashCode(value); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (!(obj instanceof Model)) + { + return false; + } + Model that = (Model)obj; + return Objects.equal(value, that.getObject()); + } +} diff --git a/src/main/java/com/gitblit/wicket/pages/EditTicketPage.java b/src/main/java/com/gitblit/wicket/pages/EditTicketPage.java index 4a06e59e..bd2ec63c 100644 --- a/src/main/java/com/gitblit/wicket/pages/EditTicketPage.java +++ b/src/main/java/com/gitblit/wicket/pages/EditTicketPage.java @@ -50,6 +50,8 @@ import com.gitblit.tickets.TicketNotifier; import com.gitblit.tickets.TicketResponsible; import com.gitblit.utils.StringUtils; import com.gitblit.wicket.GitBlitWebSession; +import com.gitblit.wicket.SafeTextModel; +import com.gitblit.wicket.SafeTextModel.Mode; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.panels.MarkdownTextArea; @@ -110,8 +112,8 @@ public class EditTicketPage extends RepositoryPage { } typeModel = Model.of(ticket.type); - titleModel = Model.of(ticket.title); - topicModel = Model.of(ticket.topic == null ? "" : ticket.topic); + titleModel = SafeTextModel.none(ticket.title); + topicModel = SafeTextModel.none(ticket.topic == null ? "" : ticket.topic); responsibleModel = Model.of(); milestoneModel = Model.of(); mergeToModel = Model.of(ticket.mergeTo == null ? getRepositoryModel().mergeTo : ticket.mergeTo); @@ -134,7 +136,7 @@ public class EditTicketPage extends RepositoryPage { form.add(new TextField("title", titleModel)); form.add(new TextField("topic", topicModel)); - final IModel markdownPreviewModel = new Model(); + final SafeTextModel markdownPreviewModel = new SafeTextModel(Mode.none); descriptionPreview = new Label("descriptionPreview", markdownPreviewModel); descriptionPreview.setEscapeModelStrings(false); descriptionPreview.setOutputMarkupId(true); diff --git a/src/main/java/com/gitblit/wicket/pages/NewTicketPage.java b/src/main/java/com/gitblit/wicket/pages/NewTicketPage.java index 961590a2..7a68febb 100644 --- a/src/main/java/com/gitblit/wicket/pages/NewTicketPage.java +++ b/src/main/java/com/gitblit/wicket/pages/NewTicketPage.java @@ -46,6 +46,8 @@ import com.gitblit.tickets.TicketNotifier; import com.gitblit.tickets.TicketResponsible; import com.gitblit.utils.StringUtils; import com.gitblit.wicket.GitBlitWebSession; +import com.gitblit.wicket.SafeTextModel; +import com.gitblit.wicket.SafeTextModel.Mode; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.panels.MarkdownTextArea; @@ -87,8 +89,8 @@ public class NewTicketPage extends RepositoryPage { } typeModel = Model.of(TicketModel.Type.defaultType); - titleModel = Model.of(); - topicModel = Model.of(); + titleModel = SafeTextModel.none(); + topicModel = SafeTextModel.none(); mergeToModel = Model.of(Repository.shortenRefName(getRepositoryModel().mergeTo)); responsibleModel = Model.of(); milestoneModel = Model.of(); @@ -103,7 +105,7 @@ public class NewTicketPage extends RepositoryPage { form.add(new TextField("title", titleModel)); form.add(new TextField("topic", topicModel)); - final IModel markdownPreviewModel = new Model(); + final SafeTextModel markdownPreviewModel = new SafeTextModel(Mode.none); descriptionPreview = new Label("descriptionPreview", markdownPreviewModel); descriptionPreview.setEscapeModelStrings(false); descriptionPreview.setOutputMarkupId(true); diff --git a/src/main/java/com/gitblit/wicket/panels/CommentPanel.java b/src/main/java/com/gitblit/wicket/panels/CommentPanel.java index 1d49ff0f..130e7336 100644 --- a/src/main/java/com/gitblit/wicket/panels/CommentPanel.java +++ b/src/main/java/com/gitblit/wicket/panels/CommentPanel.java @@ -19,13 +19,14 @@ import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.markup.html.form.AjaxButton; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.Form; -import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import com.gitblit.models.RepositoryModel; import com.gitblit.models.TicketModel; import com.gitblit.models.TicketModel.Change; import com.gitblit.models.UserModel; +import com.gitblit.wicket.SafeTextModel; +import com.gitblit.wicket.SafeTextModel.Mode; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.pages.BasePage; @@ -89,7 +90,7 @@ public class CommentPanel extends BasePanel { } }.setVisible(ticket != null && ticket.number > 0)); - final IModel markdownPreviewModel = new Model(); + final SafeTextModel markdownPreviewModel = new SafeTextModel(Mode.none); markdownPreview = new Label("markdownPreview", markdownPreviewModel); markdownPreview.setEscapeModelStrings(false); markdownPreview.setOutputMarkupId(true); diff --git a/src/main/java/com/gitblit/wicket/panels/MarkdownTextArea.java b/src/main/java/com/gitblit/wicket/panels/MarkdownTextArea.java index f26f7fbe..6e06e5bb 100644 --- a/src/main/java/com/gitblit/wicket/panels/MarkdownTextArea.java +++ b/src/main/java/com/gitblit/wicket/panels/MarkdownTextArea.java @@ -20,12 +20,12 @@ import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.TextArea; -import org.apache.wicket.model.IModel; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.util.time.Duration; import com.gitblit.utils.MarkdownUtils; import com.gitblit.wicket.GitBlitWebApp; +import com.gitblit.wicket.SafeTextModel; public class MarkdownTextArea extends TextArea { @@ -35,7 +35,7 @@ public class MarkdownTextArea extends TextArea { protected String text = ""; - public MarkdownTextArea(String id, final IModel previewModel, final Label previewLabel) { + public MarkdownTextArea(String id, final SafeTextModel previewModel, final Label previewLabel) { super(id); setModel(new PropertyModel(this, "text")); add(new AjaxFormComponentUpdatingBehavior("onblur") { @@ -65,7 +65,7 @@ public class MarkdownTextArea extends TextArea { setOutputMarkupId(true); } - protected void renderPreview(IModel previewModel) { + protected void renderPreview(SafeTextModel previewModel) { if (text == null) { return; } -- cgit v1.2.3 From 11a1739389e9bafa0b89de910105967508b56dbf Mon Sep 17 00:00:00 2001 From: James Moger Date: Sun, 7 Sep 2014 11:21:59 -0400 Subject: Enforce relaxed XSS filtering on markup documents --- .../java/com/gitblit/wicket/MarkupProcessor.java | 930 +++++++++++---------- src/main/java/com/gitblit/wicket/WicketUtils.java | 5 +- .../java/com/gitblit/wicket/pages/BlobPage.java | 2 +- .../java/com/gitblit/wicket/pages/DocPage.java | 2 +- .../java/com/gitblit/wicket/pages/DocsPage.java | 2 +- .../java/com/gitblit/wicket/pages/SummaryPage.java | 2 +- 6 files changed, 480 insertions(+), 463 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/wicket/MarkupProcessor.java b/src/main/java/com/gitblit/wicket/MarkupProcessor.java index e7681f2c..b2032049 100644 --- a/src/main/java/com/gitblit/wicket/MarkupProcessor.java +++ b/src/main/java/com/gitblit/wicket/MarkupProcessor.java @@ -1,457 +1,473 @@ -/* - * Copyright 2013 gitblit.com. - * - * 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 com.gitblit.wicket; - -import static org.pegdown.FastEncoder.encode; - -import java.io.Serializable; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.wicket.Page; -import org.apache.wicket.RequestCycle; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.mylyn.wikitext.confluence.core.ConfluenceLanguage; -import org.eclipse.mylyn.wikitext.core.parser.Attributes; -import org.eclipse.mylyn.wikitext.core.parser.MarkupParser; -import org.eclipse.mylyn.wikitext.core.parser.builder.HtmlDocumentBuilder; -import org.eclipse.mylyn.wikitext.core.parser.markup.MarkupLanguage; -import org.eclipse.mylyn.wikitext.mediawiki.core.MediaWikiLanguage; -import org.eclipse.mylyn.wikitext.textile.core.TextileLanguage; -import org.eclipse.mylyn.wikitext.tracwiki.core.TracWikiLanguage; -import org.eclipse.mylyn.wikitext.twiki.core.TWikiLanguage; -import org.pegdown.DefaultVerbatimSerializer; -import org.pegdown.LinkRenderer; -import org.pegdown.ToHtmlSerializer; -import org.pegdown.VerbatimSerializer; -import org.pegdown.ast.ExpImageNode; -import org.pegdown.ast.RefImageNode; -import org.pegdown.ast.WikiLinkNode; -import org.pegdown.plugins.ToHtmlSerializerPlugin; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.IStoredSettings; -import com.gitblit.Keys; -import com.gitblit.models.PathModel; -import com.gitblit.servlet.RawServlet; -import com.gitblit.utils.JGitUtils; -import com.gitblit.utils.MarkdownUtils; -import com.gitblit.utils.StringUtils; -import com.gitblit.wicket.pages.DocPage; -import com.google.common.base.Joiner; - -/** - * Processes markup content and generates html with repository-relative page and - * image linking. - * - * @author James Moger - * - */ -public class MarkupProcessor { - - public enum MarkupSyntax { - PLAIN, MARKDOWN, TWIKI, TRACWIKI, TEXTILE, MEDIAWIKI, CONFLUENCE - } - - private Logger logger = LoggerFactory.getLogger(getClass()); - - private final IStoredSettings settings; - - public MarkupProcessor(IStoredSettings settings) { - this.settings = settings; - } - - public List getMarkupExtensions() { - List list = new ArrayList(); - list.addAll(settings.getStrings(Keys.web.confluenceExtensions)); - list.addAll(settings.getStrings(Keys.web.markdownExtensions)); - list.addAll(settings.getStrings(Keys.web.mediawikiExtensions)); - list.addAll(settings.getStrings(Keys.web.textileExtensions)); - list.addAll(settings.getStrings(Keys.web.tracwikiExtensions)); - list.addAll(settings.getStrings(Keys.web.twikiExtensions)); - return list; - } - - public List getAllExtensions() { - List list = getMarkupExtensions(); - list.add("txt"); - list.add("TXT"); - return list; - } - - private List getRoots() { - return settings.getStrings(Keys.web.documents); - } - - private String [] getEncodings() { - return settings.getStrings(Keys.web.blobEncodings).toArray(new String[0]); - } - - private MarkupSyntax determineSyntax(String documentPath) { - String ext = StringUtils.getFileExtension(documentPath).toLowerCase(); - if (StringUtils.isEmpty(ext)) { - return MarkupSyntax.PLAIN; - } - - if (settings.getStrings(Keys.web.confluenceExtensions).contains(ext)) { - return MarkupSyntax.CONFLUENCE; - } else if (settings.getStrings(Keys.web.markdownExtensions).contains(ext)) { - return MarkupSyntax.MARKDOWN; - } else if (settings.getStrings(Keys.web.mediawikiExtensions).contains(ext)) { - return MarkupSyntax.MEDIAWIKI; - } else if (settings.getStrings(Keys.web.textileExtensions).contains(ext)) { - return MarkupSyntax.TEXTILE; - } else if (settings.getStrings(Keys.web.tracwikiExtensions).contains(ext)) { - return MarkupSyntax.TRACWIKI; - } else if (settings.getStrings(Keys.web.twikiExtensions).contains(ext)) { - return MarkupSyntax.TWIKI; - } - - return MarkupSyntax.PLAIN; - } - - public boolean hasRootDocs(Repository r) { - List roots = getRoots(); - List extensions = getAllExtensions(); - List paths = JGitUtils.getFilesInPath(r, null, null); - for (PathModel path : paths) { - if (!path.isTree()) { - String ext = StringUtils.getFileExtension(path.name).toLowerCase(); - String name = StringUtils.stripFileExtension(path.name).toLowerCase(); - - if (roots.contains(name)) { - if (StringUtils.isEmpty(ext) || extensions.contains(ext)) { - return true; - } - } - } - } - return false; - } - - public List getRootDocs(Repository r, String repositoryName, String commitId) { - List roots = getRoots(); - List list = getDocs(r, repositoryName, commitId, roots); - return list; - } - - public MarkupDocument getReadme(Repository r, String repositoryName, String commitId) { - List list = getDocs(r, repositoryName, commitId, Arrays.asList("readme")); - if (list.isEmpty()) { - return null; - } - return list.get(0); - } - - private List getDocs(Repository r, String repositoryName, String commitId, List names) { - List extensions = getAllExtensions(); - String [] encodings = getEncodings(); - Map map = new HashMap(); - RevCommit commit = JGitUtils.getCommit(r, commitId); - List paths = JGitUtils.getFilesInPath(r, null, commit); - for (PathModel path : paths) { - if (!path.isTree()) { - String ext = StringUtils.getFileExtension(path.name).toLowerCase(); - String name = StringUtils.stripFileExtension(path.name).toLowerCase(); - - if (names.contains(name)) { - if (StringUtils.isEmpty(ext) || extensions.contains(ext)) { - String markup = JGitUtils.getStringContent(r, commit.getTree(), path.name, encodings); - MarkupDocument doc = parse(repositoryName, commitId, path.name, markup); - map.put(name, doc); - } - } - } - } - // return document list in requested order - List list = new ArrayList(); - for (String name : names) { - if (map.containsKey(name)) { - list.add(map.get(name)); - } - } - return list; - } - - public MarkupDocument parse(String repositoryName, String commitId, String documentPath, String markupText) { - final MarkupSyntax syntax = determineSyntax(documentPath); - final MarkupDocument doc = new MarkupDocument(documentPath, markupText, syntax); - - if (markupText != null) { - try { - switch (syntax){ - case CONFLUENCE: - parse(doc, repositoryName, commitId, new ConfluenceLanguage()); - break; - case MARKDOWN: - parse(doc, repositoryName, commitId); - break; - case MEDIAWIKI: - parse(doc, repositoryName, commitId, new MediaWikiLanguage()); - break; - case TEXTILE: - parse(doc, repositoryName, commitId, new TextileLanguage()); - break; - case TRACWIKI: - parse(doc, repositoryName, commitId, new TracWikiLanguage()); - break; - case TWIKI: - parse(doc, repositoryName, commitId, new TWikiLanguage()); - break; - default: - doc.html = MarkdownUtils.transformPlainText(markupText); - break; - } - } catch (Exception e) { - logger.error("failed to transform " + syntax, e); - } - } - - if (doc.html == null) { - // failed to transform markup - if (markupText == null) { - markupText = String.format("Document %1$s not found in %2$s", documentPath, repositoryName); - } - markupText = MessageFormat.format("
{0}: {1}
{2}", "Error", "failed to parse markup", markupText); - doc.html = StringUtils.breakLinesForHtml(markupText); - } - - return doc; - } - - /** - * Parses the markup using the specified markup language - * - * @param doc - * @param repositoryName - * @param commitId - * @param lang - */ - private void parse(final MarkupDocument doc, final String repositoryName, final String commitId, MarkupLanguage lang) { - StringWriter writer = new StringWriter(); - HtmlDocumentBuilder builder = new HtmlDocumentBuilder(writer) { - - @Override - public void image(Attributes attributes, String imagePath) { - String url; - if (imagePath.indexOf("://") == -1) { - // relative image - String path = doc.getRelativePath(imagePath); - String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot(); - url = RawServlet.asLink(contextUrl, repositoryName, commitId, path); - } else { - // absolute image - url = imagePath; - } - super.image(attributes, url); - } - - @Override - public void link(Attributes attributes, String hrefOrHashName, String text) { - String url; - if (hrefOrHashName.charAt(0) != '#') { - if (hrefOrHashName.indexOf("://") == -1) { - // relative link - String path = doc.getRelativePath(hrefOrHashName); - url = getWicketUrl(DocPage.class, repositoryName, commitId, path); - } else { - // absolute link - url = hrefOrHashName; - } - } else { - // page-relative hash link - url = hrefOrHashName; - } - super.link(attributes, url, text); - } - }; - - // avoid the and tags - builder.setEmitAsDocument(false); - - MarkupParser parser = new MarkupParser(lang); - parser.setBuilder(builder); - parser.parse(doc.markup); - doc.html = writer.toString(); - } - - /** - * Parses the document as Markdown using Pegdown. - * - * @param doc - * @param repositoryName - * @param commitId - */ - private void parse(final MarkupDocument doc, final String repositoryName, final String commitId) { - LinkRenderer renderer = new LinkRenderer() { - - @Override - public Rendering render(ExpImageNode node, String text) { - if (node.url.indexOf("://") == -1) { - // repository-relative image link - String path = doc.getRelativePath(node.url); - String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot(); - String url = RawServlet.asLink(contextUrl, repositoryName, commitId, path); - return new Rendering(url, text); - } - // absolute image link - return new Rendering(node.url, text); - } - - @Override - public Rendering render(RefImageNode node, String url, String title, String alt) { - Rendering rendering; - if (url.indexOf("://") == -1) { - // repository-relative image link - String path = doc.getRelativePath(url); - String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot(); - String wurl = RawServlet.asLink(contextUrl, repositoryName, commitId, path); - rendering = new Rendering(wurl, alt); - } else { - // absolute image link - rendering = new Rendering(url, alt); - } - return StringUtils.isEmpty(title) ? rendering : rendering.withAttribute("title", encode(title)); - } - - @Override - public Rendering render(WikiLinkNode node) { - String path = doc.getRelativePath(node.getText()); - String name = getDocumentName(path); - String url = getWicketUrl(DocPage.class, repositoryName, commitId, path); - return new Rendering(url, name); - } - }; - doc.html = MarkdownUtils.transformMarkdown(doc.markup, renderer); - } - - private String getWicketUrl(Class pageClass, final String repositoryName, final String commitId, final String document) { - String fsc = settings.getString(Keys.web.forwardSlashCharacter, "/"); - String encodedPath = document.replace(' ', '-'); - try { - encodedPath = URLEncoder.encode(encodedPath, "UTF-8"); - } catch (UnsupportedEncodingException e) { - logger.error(null, e); - } - encodedPath = encodedPath.replace("/", fsc).replace("%2F", fsc); - - String url = RequestCycle.get().urlFor(pageClass, WicketUtils.newPathParameter(repositoryName, commitId, encodedPath)).toString(); - return url; - } - - private String getDocumentName(final String document) { - // extract document name - String name = StringUtils.stripFileExtension(document); - name = name.replace('_', ' '); - if (name.indexOf('/') > -1) { - name = name.substring(name.lastIndexOf('/') + 1); - } - return name; - } - - public static class MarkupDocument implements Serializable { - - private static final long serialVersionUID = 1L; - - public final String documentPath; - public final String markup; - public final MarkupSyntax syntax; - public String html; - - MarkupDocument(String documentPath, String markup, MarkupSyntax syntax) { - this.documentPath = documentPath; - this.markup = markup; - this.syntax = syntax; - } - - String getCurrentPath() { - String basePath = ""; - if (documentPath.indexOf('/') > -1) { - basePath = documentPath.substring(0, documentPath.lastIndexOf('/') + 1); - if (basePath.charAt(0) == '/') { - return basePath.substring(1); - } - } - return basePath; - } - - String getRelativePath(String ref) { - if (ref.charAt(0) == '/') { - // absolute path in repository - return ref.substring(1); - } else { - // resolve relative repository path - String cp = getCurrentPath(); - if (StringUtils.isEmpty(cp)) { - return ref; - } - // this is a simple relative path resolver - List currPathStrings = new ArrayList(Arrays.asList(cp.split("/"))); - String file = ref; - while (file.startsWith("../")) { - // strip ../ from the file reference - // drop the last path element - file = file.substring(3); - currPathStrings.remove(currPathStrings.size() - 1); - } - currPathStrings.add(file); - String path = Joiner.on("/").join(currPathStrings); - return path; - } - } - } - - /** - * This class implements a workaround for a bug reported in issue-379. - * The bug was introduced by my own pegdown pull request #115. - * - * @author James Moger - * - */ - public static class WorkaroundHtmlSerializer extends ToHtmlSerializer { - - public WorkaroundHtmlSerializer(final LinkRenderer linkRenderer) { - super(linkRenderer, - Collections.singletonMap(VerbatimSerializer.DEFAULT, DefaultVerbatimSerializer.INSTANCE), - Collections.emptyList()); - } - private void printAttribute(String name, String value) { - printer.print(' ').print(name).print('=').print('"').print(value).print('"'); - } - - /* Reimplement print image tag to eliminate a trailing double-quote */ - @Override - protected void printImageTag(LinkRenderer.Rendering rendering) { - printer.print(""); - } - } -} +/* + * Copyright 2013 gitblit.com. + * + * 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 com.gitblit.wicket; + +import static org.pegdown.FastEncoder.encode; + +import java.io.Serializable; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.wicket.Page; +import org.apache.wicket.RequestCycle; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.mylyn.wikitext.confluence.core.ConfluenceLanguage; +import org.eclipse.mylyn.wikitext.core.parser.Attributes; +import org.eclipse.mylyn.wikitext.core.parser.MarkupParser; +import org.eclipse.mylyn.wikitext.core.parser.builder.HtmlDocumentBuilder; +import org.eclipse.mylyn.wikitext.core.parser.markup.MarkupLanguage; +import org.eclipse.mylyn.wikitext.mediawiki.core.MediaWikiLanguage; +import org.eclipse.mylyn.wikitext.textile.core.TextileLanguage; +import org.eclipse.mylyn.wikitext.tracwiki.core.TracWikiLanguage; +import org.eclipse.mylyn.wikitext.twiki.core.TWikiLanguage; +import org.pegdown.DefaultVerbatimSerializer; +import org.pegdown.LinkRenderer; +import org.pegdown.ToHtmlSerializer; +import org.pegdown.VerbatimSerializer; +import org.pegdown.ast.ExpImageNode; +import org.pegdown.ast.RefImageNode; +import org.pegdown.ast.WikiLinkNode; +import org.pegdown.plugins.ToHtmlSerializerPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.IStoredSettings; +import com.gitblit.Keys; +import com.gitblit.models.PathModel; +import com.gitblit.servlet.RawServlet; +import com.gitblit.utils.JGitUtils; +import com.gitblit.utils.MarkdownUtils; +import com.gitblit.utils.StringUtils; +import com.gitblit.utils.XssFilter; +import com.gitblit.wicket.pages.DocPage; +import com.google.common.base.Joiner; + +/** + * Processes markup content and generates html with repository-relative page and + * image linking. + * + * @author James Moger + * + */ +public class MarkupProcessor { + + public enum MarkupSyntax { + PLAIN, MARKDOWN, TWIKI, TRACWIKI, TEXTILE, MEDIAWIKI, CONFLUENCE + } + + private Logger logger = LoggerFactory.getLogger(getClass()); + + private final IStoredSettings settings; + + private final XssFilter xssFilter; + + public static List getMarkupExtensions(IStoredSettings settings) { + List list = new ArrayList(); + list.addAll(settings.getStrings(Keys.web.confluenceExtensions)); + list.addAll(settings.getStrings(Keys.web.markdownExtensions)); + list.addAll(settings.getStrings(Keys.web.mediawikiExtensions)); + list.addAll(settings.getStrings(Keys.web.textileExtensions)); + list.addAll(settings.getStrings(Keys.web.tracwikiExtensions)); + list.addAll(settings.getStrings(Keys.web.twikiExtensions)); + return list; + } + + public MarkupProcessor(IStoredSettings settings, XssFilter xssFilter) { + this.settings = settings; + this.xssFilter = xssFilter; + } + + public List getMarkupExtensions() { + return getMarkupExtensions(settings); + } + + public List getAllExtensions() { + List list = getMarkupExtensions(settings); + list.add("txt"); + list.add("TXT"); + return list; + } + + private List getRoots() { + return settings.getStrings(Keys.web.documents); + } + + private String [] getEncodings() { + return settings.getStrings(Keys.web.blobEncodings).toArray(new String[0]); + } + + private MarkupSyntax determineSyntax(String documentPath) { + String ext = StringUtils.getFileExtension(documentPath).toLowerCase(); + if (StringUtils.isEmpty(ext)) { + return MarkupSyntax.PLAIN; + } + + if (settings.getStrings(Keys.web.confluenceExtensions).contains(ext)) { + return MarkupSyntax.CONFLUENCE; + } else if (settings.getStrings(Keys.web.markdownExtensions).contains(ext)) { + return MarkupSyntax.MARKDOWN; + } else if (settings.getStrings(Keys.web.mediawikiExtensions).contains(ext)) { + return MarkupSyntax.MEDIAWIKI; + } else if (settings.getStrings(Keys.web.textileExtensions).contains(ext)) { + return MarkupSyntax.TEXTILE; + } else if (settings.getStrings(Keys.web.tracwikiExtensions).contains(ext)) { + return MarkupSyntax.TRACWIKI; + } else if (settings.getStrings(Keys.web.twikiExtensions).contains(ext)) { + return MarkupSyntax.TWIKI; + } + + return MarkupSyntax.PLAIN; + } + + public boolean hasRootDocs(Repository r) { + List roots = getRoots(); + List extensions = getAllExtensions(); + List paths = JGitUtils.getFilesInPath(r, null, null); + for (PathModel path : paths) { + if (!path.isTree()) { + String ext = StringUtils.getFileExtension(path.name).toLowerCase(); + String name = StringUtils.stripFileExtension(path.name).toLowerCase(); + + if (roots.contains(name)) { + if (StringUtils.isEmpty(ext) || extensions.contains(ext)) { + return true; + } + } + } + } + return false; + } + + public List getRootDocs(Repository r, String repositoryName, String commitId) { + List roots = getRoots(); + List list = getDocs(r, repositoryName, commitId, roots); + return list; + } + + public MarkupDocument getReadme(Repository r, String repositoryName, String commitId) { + List list = getDocs(r, repositoryName, commitId, Arrays.asList("readme")); + if (list.isEmpty()) { + return null; + } + return list.get(0); + } + + private List getDocs(Repository r, String repositoryName, String commitId, List names) { + List extensions = getAllExtensions(); + String [] encodings = getEncodings(); + Map map = new HashMap(); + RevCommit commit = JGitUtils.getCommit(r, commitId); + List paths = JGitUtils.getFilesInPath(r, null, commit); + for (PathModel path : paths) { + if (!path.isTree()) { + String ext = StringUtils.getFileExtension(path.name).toLowerCase(); + String name = StringUtils.stripFileExtension(path.name).toLowerCase(); + + if (names.contains(name)) { + if (StringUtils.isEmpty(ext) || extensions.contains(ext)) { + String markup = JGitUtils.getStringContent(r, commit.getTree(), path.name, encodings); + MarkupDocument doc = parse(repositoryName, commitId, path.name, markup); + map.put(name, doc); + } + } + } + } + // return document list in requested order + List list = new ArrayList(); + for (String name : names) { + if (map.containsKey(name)) { + list.add(map.get(name)); + } + } + return list; + } + + public MarkupDocument parse(String repositoryName, String commitId, String documentPath, String markupText) { + final MarkupSyntax syntax = determineSyntax(documentPath); + final MarkupDocument doc = new MarkupDocument(documentPath, markupText, syntax); + + if (markupText != null) { + try { + switch (syntax){ + case CONFLUENCE: + parse(doc, repositoryName, commitId, new ConfluenceLanguage()); + break; + case MARKDOWN: + parse(doc, repositoryName, commitId); + break; + case MEDIAWIKI: + parse(doc, repositoryName, commitId, new MediaWikiLanguage()); + break; + case TEXTILE: + parse(doc, repositoryName, commitId, new TextileLanguage()); + break; + case TRACWIKI: + parse(doc, repositoryName, commitId, new TracWikiLanguage()); + break; + case TWIKI: + parse(doc, repositoryName, commitId, new TWikiLanguage()); + break; + default: + doc.html = MarkdownUtils.transformPlainText(markupText); + break; + } + } catch (Exception e) { + logger.error("failed to transform " + syntax, e); + } + } + + if (doc.html == null) { + // failed to transform markup + if (markupText == null) { + markupText = String.format("Document %1$s not found in %2$s", documentPath, repositoryName); + } + markupText = MessageFormat.format("
{0}: {1}
{2}", "Error", "failed to parse markup", markupText); + doc.html = StringUtils.breakLinesForHtml(markupText); + } + + return doc; + } + + /** + * Parses the markup using the specified markup language + * + * @param doc + * @param repositoryName + * @param commitId + * @param lang + */ + private void parse(final MarkupDocument doc, final String repositoryName, final String commitId, MarkupLanguage lang) { + StringWriter writer = new StringWriter(); + HtmlDocumentBuilder builder = new HtmlDocumentBuilder(writer) { + + @Override + public void image(Attributes attributes, String imagePath) { + String url; + if (imagePath.indexOf("://") == -1) { + // relative image + String path = doc.getRelativePath(imagePath); + String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot(); + url = RawServlet.asLink(contextUrl, repositoryName, commitId, path); + } else { + // absolute image + url = imagePath; + } + super.image(attributes, url); + } + + @Override + public void link(Attributes attributes, String hrefOrHashName, String text) { + String url; + if (hrefOrHashName.charAt(0) != '#') { + if (hrefOrHashName.indexOf("://") == -1) { + // relative link + String path = doc.getRelativePath(hrefOrHashName); + url = getWicketUrl(DocPage.class, repositoryName, commitId, path); + } else { + // absolute link + url = hrefOrHashName; + } + } else { + // page-relative hash link + url = hrefOrHashName; + } + super.link(attributes, url, text); + } + }; + + // avoid the and tags + builder.setEmitAsDocument(false); + + MarkupParser parser = new MarkupParser(lang); + parser.setBuilder(builder); + parser.parse(doc.markup); + + final String content = writer.toString(); + final String safeContent = xssFilter.relaxed(content); + + doc.html = safeContent; + } + + /** + * Parses the document as Markdown using Pegdown. + * + * @param doc + * @param repositoryName + * @param commitId + */ + private void parse(final MarkupDocument doc, final String repositoryName, final String commitId) { + LinkRenderer renderer = new LinkRenderer() { + + @Override + public Rendering render(ExpImageNode node, String text) { + if (node.url.indexOf("://") == -1) { + // repository-relative image link + String path = doc.getRelativePath(node.url); + String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot(); + String url = RawServlet.asLink(contextUrl, repositoryName, commitId, path); + return new Rendering(url, text); + } + // absolute image link + return new Rendering(node.url, text); + } + + @Override + public Rendering render(RefImageNode node, String url, String title, String alt) { + Rendering rendering; + if (url.indexOf("://") == -1) { + // repository-relative image link + String path = doc.getRelativePath(url); + String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot(); + String wurl = RawServlet.asLink(contextUrl, repositoryName, commitId, path); + rendering = new Rendering(wurl, alt); + } else { + // absolute image link + rendering = new Rendering(url, alt); + } + return StringUtils.isEmpty(title) ? rendering : rendering.withAttribute("title", encode(title)); + } + + @Override + public Rendering render(WikiLinkNode node) { + String path = doc.getRelativePath(node.getText()); + String name = getDocumentName(path); + String url = getWicketUrl(DocPage.class, repositoryName, commitId, path); + return new Rendering(url, name); + } + }; + + final String content = MarkdownUtils.transformMarkdown(doc.markup, renderer); + final String safeContent = xssFilter.relaxed(content); + + doc.html = safeContent; + } + + private String getWicketUrl(Class pageClass, final String repositoryName, final String commitId, final String document) { + String fsc = settings.getString(Keys.web.forwardSlashCharacter, "/"); + String encodedPath = document.replace(' ', '-'); + try { + encodedPath = URLEncoder.encode(encodedPath, "UTF-8"); + } catch (UnsupportedEncodingException e) { + logger.error(null, e); + } + encodedPath = encodedPath.replace("/", fsc).replace("%2F", fsc); + + String url = RequestCycle.get().urlFor(pageClass, WicketUtils.newPathParameter(repositoryName, commitId, encodedPath)).toString(); + return url; + } + + private String getDocumentName(final String document) { + // extract document name + String name = StringUtils.stripFileExtension(document); + name = name.replace('_', ' '); + if (name.indexOf('/') > -1) { + name = name.substring(name.lastIndexOf('/') + 1); + } + return name; + } + + public static class MarkupDocument implements Serializable { + + private static final long serialVersionUID = 1L; + + public final String documentPath; + public final String markup; + public final MarkupSyntax syntax; + public String html; + + MarkupDocument(String documentPath, String markup, MarkupSyntax syntax) { + this.documentPath = documentPath; + this.markup = markup; + this.syntax = syntax; + } + + String getCurrentPath() { + String basePath = ""; + if (documentPath.indexOf('/') > -1) { + basePath = documentPath.substring(0, documentPath.lastIndexOf('/') + 1); + if (basePath.charAt(0) == '/') { + return basePath.substring(1); + } + } + return basePath; + } + + String getRelativePath(String ref) { + if (ref.charAt(0) == '/') { + // absolute path in repository + return ref.substring(1); + } else { + // resolve relative repository path + String cp = getCurrentPath(); + if (StringUtils.isEmpty(cp)) { + return ref; + } + // this is a simple relative path resolver + List currPathStrings = new ArrayList(Arrays.asList(cp.split("/"))); + String file = ref; + while (file.startsWith("../")) { + // strip ../ from the file reference + // drop the last path element + file = file.substring(3); + currPathStrings.remove(currPathStrings.size() - 1); + } + currPathStrings.add(file); + String path = Joiner.on("/").join(currPathStrings); + return path; + } + } + } + + /** + * This class implements a workaround for a bug reported in issue-379. + * The bug was introduced by my own pegdown pull request #115. + * + * @author James Moger + * + */ + public static class WorkaroundHtmlSerializer extends ToHtmlSerializer { + + public WorkaroundHtmlSerializer(final LinkRenderer linkRenderer) { + super(linkRenderer, + Collections.singletonMap(VerbatimSerializer.DEFAULT, DefaultVerbatimSerializer.INSTANCE), + Collections.emptyList()); + } + private void printAttribute(String name, String value) { + printer.print(' ').print(name).print('=').print('"').print(value).print('"'); + } + + /* Reimplement print image tag to eliminate a trailing double-quote */ + @Override + protected void printImageTag(LinkRenderer.Rendering rendering) { + printer.print(""); + } + } +} diff --git a/src/main/java/com/gitblit/wicket/WicketUtils.java b/src/main/java/com/gitblit/wicket/WicketUtils.java index 687f0105..d47390d4 100644 --- a/src/main/java/com/gitblit/wicket/WicketUtils.java +++ b/src/main/java/com/gitblit/wicket/WicketUtils.java @@ -42,6 +42,7 @@ import org.eclipse.jgit.diff.DiffEntry.ChangeType; import com.gitblit.Constants; import com.gitblit.Constants.AccessPermission; import com.gitblit.Constants.FederationPullStatus; +import com.gitblit.IStoredSettings; import com.gitblit.Keys; import com.gitblit.models.FederationModel; import com.gitblit.models.Metric; @@ -186,9 +187,9 @@ public class WicketUtils { return newImage(wicketId, "file_settings_16x16.png"); } - MarkupProcessor processor = new MarkupProcessor(GitBlitWebApp.get().settings()); String ext = StringUtils.getFileExtension(filename).toLowerCase(); - if (processor.getMarkupExtensions().contains(ext)) { + IStoredSettings settings = GitBlitWebApp.get().settings(); + if (MarkupProcessor.getMarkupExtensions(settings).contains(ext)) { return newImage(wicketId, "file_world_16x16.png"); } return newImage(wicketId, "file_16x16.png"); diff --git a/src/main/java/com/gitblit/wicket/pages/BlobPage.java b/src/main/java/com/gitblit/wicket/pages/BlobPage.java index 0938fcde..e84056b3 100644 --- a/src/main/java/com/gitblit/wicket/pages/BlobPage.java +++ b/src/main/java/com/gitblit/wicket/pages/BlobPage.java @@ -79,7 +79,7 @@ public class BlobPage extends RepositoryPage { } // see if we should redirect to the doc page - MarkupProcessor processor = new MarkupProcessor(app().settings()); + MarkupProcessor processor = new MarkupProcessor(app().settings(), app().xssFilter()); for (String ext : processor.getMarkupExtensions()) { if (ext.equals(extension)) { setResponsePage(DocPage.class, params); diff --git a/src/main/java/com/gitblit/wicket/pages/DocPage.java b/src/main/java/com/gitblit/wicket/pages/DocPage.java index c06d8065..567c6fbd 100644 --- a/src/main/java/com/gitblit/wicket/pages/DocPage.java +++ b/src/main/java/com/gitblit/wicket/pages/DocPage.java @@ -43,7 +43,7 @@ public class DocPage extends RepositoryPage { super(params); final String path = WicketUtils.getPath(params).replace("%2f", "/").replace("%2F", "/"); - MarkupProcessor processor = new MarkupProcessor(app().settings()); + MarkupProcessor processor = new MarkupProcessor(app().settings(), app().xssFilter()); Repository r = getRepository(); RevCommit commit = JGitUtils.getCommit(r, objectId); diff --git a/src/main/java/com/gitblit/wicket/pages/DocsPage.java b/src/main/java/com/gitblit/wicket/pages/DocsPage.java index fc56ee07..a3d0f214 100644 --- a/src/main/java/com/gitblit/wicket/pages/DocsPage.java +++ b/src/main/java/com/gitblit/wicket/pages/DocsPage.java @@ -49,7 +49,7 @@ public class DocsPage extends RepositoryPage { public DocsPage(PageParameters params) { super(params); - MarkupProcessor processor = new MarkupProcessor(app().settings()); + MarkupProcessor processor = new MarkupProcessor(app().settings(), app().xssFilter()); Repository r = getRepository(); RevCommit head = JGitUtils.getCommit(r, null); diff --git a/src/main/java/com/gitblit/wicket/pages/SummaryPage.java b/src/main/java/com/gitblit/wicket/pages/SummaryPage.java index 090c0952..3cfa152e 100644 --- a/src/main/java/com/gitblit/wicket/pages/SummaryPage.java +++ b/src/main/java/com/gitblit/wicket/pages/SummaryPage.java @@ -138,7 +138,7 @@ public class SummaryPage extends RepositoryPage { MarkupDocument markupDoc = null; RevCommit head = JGitUtils.getCommit(r, null); if (head != null) { - MarkupProcessor processor = new MarkupProcessor(app().settings()); + MarkupProcessor processor = new MarkupProcessor(app().settings(), app().xssFilter()); markupDoc = processor.getReadme(r, repositoryName, getBestCommitId(head)); } if (markupDoc == null || markupDoc.markup == null) { -- cgit v1.2.3 From 7fdc298cf06c3d88d4fd9fd158fb4d32edac12a0 Mon Sep 17 00:00:00 2001 From: James Moger Date: Sun, 7 Sep 2014 11:52:53 -0400 Subject: Apply the relaxed XSS filter to Markdown commit messages --- src/main/java/com/gitblit/wicket/pages/RepositoryPage.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java index 253c4fe4..2bd9dc6c 100644 --- a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java +++ b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java @@ -550,7 +550,8 @@ public abstract class RepositoryPage extends RootPage { String html; switch (model.commitMessageRenderer) { case MARKDOWN: - html = MessageFormat.format("
{0}
", content); + String safeContent = app().xssFilter().relaxed(content); + html = MessageFormat.format("
{0}
", safeContent); break; default: html = MessageFormat.format("
{0}
", content); -- cgit v1.2.3