From f9c661ef5d2a422f246b3a089bee06470ae1d431 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Sun, 07 Sep 2014 12:04:12 -0400
Subject: [PATCH] Merged #164 "Sanitize page parameters for XSS vulerabilities"

---
 src/main/java/com/gitblit/manager/IRuntimeManager.java              |    8 
 .classpath                                                          |    1 
 src/main/java/com/gitblit/wicket/GitBlitWebApp.java                 |   14 
 src/main/java/com/gitblit/wicket/pages/NewTicketPage.java           |    8 
 src/main/java/com/gitblit/manager/RuntimeManager.java               |   21 
 src/main/java/com/gitblit/wicket/pages/DocsPage.java                |    2 
 src/main/java/com/gitblit/wicket/pages/EditTicketPage.java          |    8 
 src/main/java/com/gitblit/wicket/pages/SummaryPage.java             |    2 
 src/main/java/com/gitblit/manager/GitblitManager.java               |    6 
 src/main/java/com/gitblit/wicket/GitblitParamUrlCodingStrategy.java |  408 +++++++------
 src/test/java/com/gitblit/tests/AuthenticationManagerTest.java      |    5 
 src/main/java/com/gitblit/wicket/WicketUtils.java                   |    5 
 src/main/java/com/gitblit/utils/XssFilter.java                      |   64 ++
 src/main/java/com/gitblit/wicket/pages/RepositoryPage.java          |    3 
 src/main/java/com/gitblit/ReindexTickets.java                       |    5 
 src/test/java/com/gitblit/tests/RedmineAuthenticationTest.java      |    8 
 src/test/java/com/gitblit/tests/FileTicketServiceTest.java          |    6 
 src/test/java/com/gitblit/tests/LdapAuthenticationTest.java         |    8 
 src/main/java/com/gitblit/utils/JSoupXssFilter.java                 |   87 ++
 src/main/java/com/gitblit/wicket/GitblitWicketApp.java              |    3 
 src/test/java/com/gitblit/tests/BranchTicketServiceTest.java        |    6 
 src/test/java/com/gitblit/tests/LuceneExecutorTest.java             |    5 
 src/main/java/com/gitblit/MigrateTickets.java                       |    5 
 src/main/java/com/gitblit/wicket/pages/BlobPage.java                |    2 
 src/main/java/com/gitblit/wicket/pages/DocPage.java                 |    2 
 src/main/java/com/gitblit/wicket/panels/CommentPanel.java           |    5 
 src/main/java/com/gitblit/DaggerModule.java                         |   11 
 src/main/java/com/gitblit/FederationClient.java                     |    5 
 src/test/java/com/gitblit/tests/RedisTicketServiceTest.java         |    6 
 src/main/java/com/gitblit/wicket/panels/MarkdownTextArea.java       |    6 
 src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java     |    8 
 build.moxie                                                         |    1 
 src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java        |    7 
 gitblit.iml                                                         |   11 
 src/main/java/com/gitblit/wicket/SafeTextModel.java                 |   96 +++
 src/main/java/com/gitblit/wicket/MarkupProcessor.java               |  930 +++++++++++++++---------------
 36 files changed, 1,092 insertions(+), 686 deletions(-)

diff --git a/.classpath b/.classpath
index f6e655e..a6b4010 100644
--- a/.classpath
+++ b/.classpath
@@ -77,6 +77,7 @@
 	<classpathentry kind="lib" path="ext/commons-pool2-2.0.jar" sourcepath="ext/src/commons-pool2-2.0.jar" />
 	<classpathentry kind="lib" path="ext/pf4j-0.8.0.jar" sourcepath="ext/src/pf4j-0.8.0.jar" />
 	<classpathentry kind="lib" path="ext/tika-core-1.5.jar" sourcepath="ext/src/tika-core-1.5.jar" />
+	<classpathentry kind="lib" path="ext/jsoup-1.7.3.jar" sourcepath="ext/src/jsoup-1.7.3.jar" />
 	<classpathentry kind="lib" path="ext/junit-4.11.jar" sourcepath="ext/src/junit-4.11.jar" />
 	<classpathentry kind="lib" path="ext/hamcrest-core-1.3.jar" sourcepath="ext/src/hamcrest-core-1.3.jar" />
 	<classpathentry kind="lib" path="ext/selenium-java-2.28.0.jar" sourcepath="ext/src/selenium-java-2.28.0.jar" />
diff --git a/build.moxie b/build.moxie
index 0801644..c558c52 100644
--- a/build.moxie
+++ b/build.moxie
@@ -176,6 +176,7 @@
 - compile 'redis.clients:jedis:2.3.1' :war
 - compile 'ro.fortsoft.pf4j:pf4j:0.8.0' :war
 - compile 'org.apache.tika:tika-core:1.5' :war
+- compile 'org.jsoup:jsoup:1.7.3' :war
 - test 'junit'
 # Dependencies for Selenium web page testing
 - test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar
diff --git a/gitblit.iml b/gitblit.iml
index 03e2896..3e6608f 100644
--- a/gitblit.iml
+++ b/gitblit.iml
@@ -801,6 +801,17 @@
         </SOURCES>
       </library>
     </orderEntry>
+    <orderEntry type="module-library">
+      <library name="jsoup-1.7.3.jar">
+        <CLASSES>
+          <root url="jar://$MODULE_DIR$/ext/jsoup-1.7.3.jar!/" />
+        </CLASSES>
+        <JAVADOC />
+        <SOURCES>
+          <root url="jar://$MODULE_DIR$/ext/src/jsoup-1.7.3.jar!/" />
+        </SOURCES>
+      </library>
+    </orderEntry>
     <orderEntry type="module-library" scope="TEST">
       <library name="junit-4.11.jar">
         <CLASSES>
diff --git a/src/main/java/com/gitblit/DaggerModule.java b/src/main/java/com/gitblit/DaggerModule.java
index 6ad3fe6..dd7e1b2 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.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 @@
 	library = true,
 	injects = {
 			IStoredSettings.class,
+			XssFilter.class,
 
 			// core managers
 			IRuntimeManager.class,
@@ -79,8 +82,12 @@
 		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 29cdefe..079355e 100644
--- a/src/main/java/com/gitblit/FederationClient.java
+++ b/src/main/java/com/gitblit/FederationClient.java
@@ -36,6 +36,8 @@
 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 @@
 		}
 
 		// 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 ad1c63e..94284ee 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.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 @@
 		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 5a61448..858436a 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.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 @@
 		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 b9ae122..2ed52d6 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.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 @@
 		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 b2d7a2b..132534c 100644
--- a/src/main/java/com/gitblit/manager/IRuntimeManager.java
+++ b/src/main/java/com/gitblit/manager/IRuntimeManager.java
@@ -24,6 +24,7 @@
 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 @@
  	 * @since 1.4.0
 	 */
 	boolean updateSettings(Map<String, String> 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 9cdc64e..219bf80 100644
--- a/src/main/java/com/gitblit/manager/RuntimeManager.java
+++ b/src/main/java/com/gitblit/manager/RuntimeManager.java
@@ -32,12 +32,15 @@
 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 {
 
 	private final Logger logger = LoggerFactory.getLogger(getClass());
 
 	private final IStoredSettings settings;
+
+	private final XssFilter xssFilter;
 
 	private final ServerStatus serverStatus;
 
@@ -47,14 +50,15 @@
 
 	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 @@
 		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 0000000..b07bcb9
--- /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 0000000..20b5105
--- /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 f63ff3d..38dbf57 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.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 @@
 
 	private final IStoredSettings settings;
 
+	private final XssFilter xssFilter;
+
 	private final IRuntimeManager runtimeManager;
 
 	private final IPluginManager pluginManager;
@@ -134,6 +137,7 @@
 
 		super();
 		this.settings = runtimeManager.getSettings();
+		this.xssFilter = runtimeManager.getXssFilter();
 		this.runtimeManager = runtimeManager;
 		this.pluginManager = pluginManager;
 		this.notificationManager = notificationManager;
@@ -251,7 +255,7 @@
 		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)) {
@@ -308,6 +312,14 @@
 	}
 
 	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#xssFilter()
+	 */
+	@Override
+	public XssFilter xssFilter() {
+		return xssFilter;
+	}
+
+	/* (non-Javadoc)
 	 * @see com.gitblit.wicket.Webapp#isDebugMode()
 	 */
 	@Override
diff --git a/src/main/java/com/gitblit/wicket/GitblitParamUrlCodingStrategy.java b/src/main/java/com/gitblit/wicket/GitblitParamUrlCodingStrategy.java
index c658394..536f88f 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 <C>
-	 * @param mountPath
-	 *            mount path (not empty)
-	 * @param bookmarkablePageClass
-	 *            class of mounted page (not null)
-	 * @param parameterNames
-	 *            the parameter names (not null)
-	 */
-	public <C extends Page> GitblitParamUrlCodingStrategy(
-			IStoredSettings settings,
-			String mountPath,
-			Class<C> 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<String, ?> parameters)
-	{
-		if (!url.endsWith("/"))
-		{
-			url.append("/");
-		}
-
-		Set<String> parameterNamesToAdd = new HashSet<String>(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 <C>
+	 * @param mountPath
+	 *            mount path (not empty)
+	 * @param bookmarkablePageClass
+	 *            class of mounted page (not null)
+	 * @param parameterNames
+	 *            the parameter names (not null)
+	 */
+	public <C extends Page> GitblitParamUrlCodingStrategy(
+			IStoredSettings settings,
+			XssFilter xssFilter,
+			String mountPath,
+			Class<C> 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<String, Object> parameterMap = (Map<String, Object>) requestParameters.getParameters();
+		for (Map.Entry<String, Object> 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<String, ?> parameters)
+	{
+		if (!url.endsWith("/"))
+		{
+			url.append("/");
+		}
+
+		Set<String> parameterNamesToAdd = new HashSet<String>(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
diff --git a/src/main/java/com/gitblit/wicket/GitblitWicketApp.java b/src/main/java/com/gitblit/wicket/GitblitWicketApp.java
index a56e699..8d3d598 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.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 abstract IStoredSettings settings();
 
+	public abstract XssFilter xssFilter();
+
 	/**
 	 * Is Gitblit running in debug mode?
 	 *
diff --git a/src/main/java/com/gitblit/wicket/MarkupProcessor.java b/src/main/java/com/gitblit/wicket/MarkupProcessor.java
index e7681f2..b203204 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<String> getMarkupExtensions() {
-		List<String> list = new ArrayList<String>();
-		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<String> getAllExtensions() {
-		List<String> list = getMarkupExtensions();
-		list.add("txt");
-		list.add("TXT");
-		return list;
-	}
-
-	private List<String> 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<String> roots = getRoots();
-		List<String> extensions = getAllExtensions();
-		List<PathModel> 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<MarkupDocument> getRootDocs(Repository r, String repositoryName, String commitId) {
-		List<String> roots = getRoots();
-		List<MarkupDocument> list = getDocs(r, repositoryName, commitId, roots);
-		return list;
-	}
-
-	public MarkupDocument getReadme(Repository r, String repositoryName, String commitId) {
-		List<MarkupDocument> list = getDocs(r, repositoryName, commitId, Arrays.asList("readme"));
-		if (list.isEmpty()) {
-			return null;
-		}
-		return list.get(0);
-	}
-
-	private List<MarkupDocument> getDocs(Repository r, String repositoryName, String commitId, List<String> names) {
-		List<String> extensions = getAllExtensions();
-		String [] encodings = getEncodings();
-		Map<String, MarkupDocument> map = new HashMap<String, MarkupDocument>();
-		RevCommit commit = JGitUtils.getCommit(r, commitId);
-		List<PathModel> 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<MarkupDocument> list = new ArrayList<MarkupDocument>();
-		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 <b>%1$s</b> not found in <em>%2$s</em>", documentPath, repositoryName);
-			}
-			markupText = MessageFormat.format("<div class=\"alert alert-error\"><strong>{0}:</strong> {1}</div>{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 <html> and <body> 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<? extends Page> 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<String> currPathStrings = new ArrayList<String>(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.<String, VerbatimSerializer>singletonMap(VerbatimSerializer.DEFAULT, DefaultVerbatimSerializer.INSTANCE),
-					 Collections.<ToHtmlSerializerPlugin>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("<img");
-	        printAttribute("src", rendering.href);
-	        printAttribute("alt", rendering.text);
-	        for (LinkRenderer.Attribute attr : rendering.attributes) {
-	            printAttribute(attr.name, attr.value);
-	        }
-	        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<String> getMarkupExtensions(IStoredSettings settings) {
+		List<String> list = new ArrayList<String>();
+		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<String> getMarkupExtensions() {
+		return getMarkupExtensions(settings);
+	}
+
+	public List<String> getAllExtensions() {
+		List<String> list = getMarkupExtensions(settings);
+		list.add("txt");
+		list.add("TXT");
+		return list;
+	}
+
+	private List<String> 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<String> roots = getRoots();
+		List<String> extensions = getAllExtensions();
+		List<PathModel> 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<MarkupDocument> getRootDocs(Repository r, String repositoryName, String commitId) {
+		List<String> roots = getRoots();
+		List<MarkupDocument> list = getDocs(r, repositoryName, commitId, roots);
+		return list;
+	}
+
+	public MarkupDocument getReadme(Repository r, String repositoryName, String commitId) {
+		List<MarkupDocument> list = getDocs(r, repositoryName, commitId, Arrays.asList("readme"));
+		if (list.isEmpty()) {
+			return null;
+		}
+		return list.get(0);
+	}
+
+	private List<MarkupDocument> getDocs(Repository r, String repositoryName, String commitId, List<String> names) {
+		List<String> extensions = getAllExtensions();
+		String [] encodings = getEncodings();
+		Map<String, MarkupDocument> map = new HashMap<String, MarkupDocument>();
+		RevCommit commit = JGitUtils.getCommit(r, commitId);
+		List<PathModel> 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<MarkupDocument> list = new ArrayList<MarkupDocument>();
+		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 <b>%1$s</b> not found in <em>%2$s</em>", documentPath, repositoryName);
+			}
+			markupText = MessageFormat.format("<div class=\"alert alert-error\"><strong>{0}:</strong> {1}</div>{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 <html> and <body> 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<? extends Page> 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<String> currPathStrings = new ArrayList<String>(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.<String, VerbatimSerializer>singletonMap(VerbatimSerializer.DEFAULT, DefaultVerbatimSerializer.INSTANCE),
+					 Collections.<ToHtmlSerializerPlugin>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("<img");
+	        printAttribute("src", rendering.href);
+	        printAttribute("alt", rendering.text);
+	        for (LinkRenderer.Attribute attr : rendering.attributes) {
+	            printAttribute(attr.name, attr.value);
+	        }
+	        printer.print("/>");
+	    }
+	}
+}
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 0000000..aef7e97
--- /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<String> {
+
+	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/WicketUtils.java b/src/main/java/com/gitblit/wicket/WicketUtils.java
index 687f010..d47390d 100644
--- a/src/main/java/com/gitblit/wicket/WicketUtils.java
+++ b/src/main/java/com/gitblit/wicket/WicketUtils.java
@@ -42,6 +42,7 @@
 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 @@
 			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 0938fcd..e84056b 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 @@
 			}
 
 			// 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 c06d806..567c6fb 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 @@
 		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 fc56ee0..a3d0f21 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 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/EditTicketPage.java b/src/main/java/com/gitblit/wicket/pages/EditTicketPage.java
index 4a06e59..bd2ec63 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.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 @@
 		}
 
 		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 @@
 		form.add(new TextField<String>("title", titleModel));
 		form.add(new TextField<String>("topic", topicModel));
 
-		final IModel<String> markdownPreviewModel = new Model<String>();
+		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 961590a..7a68feb 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.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 @@
 		}
 
 		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 @@
 		form.add(new TextField<String>("title", titleModel));
 		form.add(new TextField<String>("topic", topicModel));
 
-		final IModel<String> markdownPreviewModel = new Model<String>();
+		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/RepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
index 253c4fe..2bd9dc6 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 @@
 		String html;
 		switch (model.commitMessageRenderer) {
 		case MARKDOWN:
-			html = MessageFormat.format("<div class='commit_message'>{0}</div>", content);
+			String safeContent = app().xssFilter().relaxed(content);
+			html = MessageFormat.format("<div class='commit_message'>{0}</div>", safeContent);
 			break;
 		default:
 			html = MessageFormat.format("<pre class='commit_message'>{0}</pre>", content);
diff --git a/src/main/java/com/gitblit/wicket/pages/SummaryPage.java b/src/main/java/com/gitblit/wicket/pages/SummaryPage.java
index 090c095..3cfa152 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 @@
 			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) {
diff --git a/src/main/java/com/gitblit/wicket/panels/CommentPanel.java b/src/main/java/com/gitblit/wicket/panels/CommentPanel.java
index 1d49ff0..130e733 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.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 @@
 			}
 		}.setVisible(ticket != null && ticket.number > 0));
 
-		final IModel<String> markdownPreviewModel = new Model<String>();
+		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 f26f7fb..6e06e5b 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.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 @@
 
 	protected String text = "";
 
-	public MarkdownTextArea(String id, final IModel<String> 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 @@
 		setOutputMarkupId(true);
 	}
 
-	protected void renderPreview(IModel<String> previewModel) {
+	protected void renderPreview(SafeTextModel previewModel) {
 		if (text == null) {
 			return;
 		}
diff --git a/src/test/java/com/gitblit/tests/AuthenticationManagerTest.java b/src/test/java/com/gitblit/tests/AuthenticationManagerTest.java
index f1d2711..0cdee6c 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.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 @@
     }
 
     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 cc404ab..0a5de19 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.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 @@
 	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 6ede042..1fb2eed 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.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 @@
 	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 f4e24d4..e2bb764 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.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 @@
     }
 
     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 @@
     }
 
     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 646f7e9..7c84ecc 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.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 @@
 	}
 
 	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 @@
 	}
 
 	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 5c319e6..a8358b9 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.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 @@
 	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 b782b44..48011ad 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.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 @@
 	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 3b6b7bb..ad773b7 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.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 @@
     }
 
     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 @@
     }
 
     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 54be539..7b56362 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.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 {
 
@@ -148,6 +150,11 @@
 	}
 
 	@Override
+	public XssFilter getXssFilter() {
+		return new AllowXssFilter();
+	}
+
+	@Override
 	public boolean updateSettings(Map<String, String> updatedSettings) {
 		return settings.saveSettings(updatedSettings);
 	}

--
Gitblit v1.9.1