SAFE_METHODS = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
+
+ /**
+ * Wicket paths are protected by Wicket's own CsrfPreventionRequestCycleListener
+ * (Origin/Referer header check) registered in OnePagerApp. Spring CSRF is not needed there.
+ */
+ private final AntPathRequestMatcher wicketMatcher = new AntPathRequestMatcher("/wicket/**");
+
+ private final RequestMatcher readOnlyPostMatcher = new OrRequestMatcher(
+ new AntPathRequestMatcher("/search/search.do", "POST"),
+ new AntPathRequestMatcher("/aim/filterDesktopActivities.do", "POST"),
+ new AntPathRequestMatcher("/aim/searchDesktopActivities.do", "POST"),
+ new AntPathRequestMatcher("/aim/validateReportsFilterPicker.do", "POST"),
+ new AntPathRequestMatcher("/aim/export*.do", "POST"),
+ new AntPathRequestMatcher("/help/*Export.do", "POST"),
+ new AntPathRequestMatcher("/calendar/viewEvents.do", "POST"),
+ new AntPathRequestMatcher("/calendar/viewListEvents.do", "POST"),
+ new AntPathRequestMatcher("/calendar/viewMonthEvents.do", "POST"),
+ new AntPathRequestMatcher("/calendar/viewYearEvents.do", "POST"));
+
+ @Override
+ public boolean matches(HttpServletRequest request) {
+ // Wicket paths handled by Wicket's CsrfPreventionRequestCycleListener
+ if (wicketMatcher.matches(request)) {
+ return false;
+ }
+ if (isPublicDocTabManagerStateChange(request)) {
+ return true;
+ }
+ if (isContentRepositoryDocumentDelete(request)) {
+ return true;
+ }
+
+ String method = request.getMethod();
+ if (method != null && SAFE_METHODS.contains(method.toUpperCase(Locale.ROOT))) {
+ return false;
+ }
+
+ if (isReadOnlyDocFromTemplatePost(request)) {
+ return false;
+ }
+ if (isReadOnlyDocumentManagerPost(request)) {
+ return false;
+ }
+
+ return !readOnlyPostMatcher.matches(request);
+ }
+
+ private boolean isReadOnlyDocumentManagerPost(HttpServletRequest request) {
+ if (!"POST".equalsIgnoreCase(request.getMethod())
+ || !"/contentrepository/documentManager.do".equals(getRequestPath(request))
+ || isMultipart(request)) {
+ return false;
+ }
+
+ return "true".equalsIgnoreCase(request.getParameter("ajaxDocumentList"));
+ }
+
+ private boolean isMultipart(HttpServletRequest request) {
+ String contentType = request.getContentType();
+ return contentType != null && contentType.toLowerCase(Locale.ROOT).startsWith("multipart/form-data");
+ }
+
+ private boolean isReadOnlyDocFromTemplatePost(HttpServletRequest request) {
+ if (!"POST".equalsIgnoreCase(request.getMethod())
+ || !"/contentrepository/docFromTemplate.do".equals(getRequestPath(request))) {
+ return false;
+ }
+
+ String action = request.getParameter("actType");
+ return action == null || action.length() == 0
+ || "loadTemplates".equalsIgnoreCase(action)
+ || "getTemplate".equalsIgnoreCase(action);
+ }
+
+ private boolean isPublicDocTabManagerStateChange(HttpServletRequest request) {
+ if (!"/contentrepository/publicDocTabManager.do".equals(getRequestPath(request))) {
+ return false;
+ }
+
+ String action = request.getParameter("action");
+ return "save".equalsIgnoreCase(action)
+ || "savePositions".equalsIgnoreCase(action)
+ || "delete".equalsIgnoreCase(action);
+ }
+
+ private boolean isContentRepositoryDocumentDelete(HttpServletRequest request) {
+ return "/contentrepository/deleteForDocumentManager.do".equals(getRequestPath(request));
+ }
+
+ private String getRequestPath(HttpServletRequest request) {
+ String path = request.getServletPath();
+ if (path == null || path.length() == 0) {
+ path = request.getRequestURI();
+ String contextPath = request.getContextPath();
+ if (contextPath != null && contextPath.length() > 0 && path.startsWith(contextPath)) {
+ path = path.substring(contextPath.length());
+ }
+ }
+ return path;
+ }
+}
\ No newline at end of file
diff --git a/amp/src/main/java/org/digijava/kernel/taglib/html/FormTag.java b/amp/src/main/java/org/digijava/kernel/taglib/html/FormTag.java
index a5003eca476..0037fb08052 100644
--- a/amp/src/main/java/org/digijava/kernel/taglib/html/FormTag.java
+++ b/amp/src/main/java/org/digijava/kernel/taglib/html/FormTag.java
@@ -22,6 +22,7 @@
package org.digijava.kernel.taglib.html;
+import org.apache.commons.lang.StringEscapeUtils;
import org.apache.struts.taglib.TagUtils;
import org.apache.struts.taglib.html.Constants;
import org.digijava.kernel.request.Site;
@@ -31,7 +32,10 @@
import org.digijava.kernel.util.SiteCache;
import org.digijava.kernel.util.SiteConfigUtils;
import org.digijava.kernel.util.SiteUtils;
+import org.springframework.security.web.csrf.CsrfToken;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
@@ -161,6 +165,7 @@ public int doStartTag() throws JspException {
results.append(this.renderFormStart());
results.append(renderToken());
+ results.append(renderSpringCsrfToken(request));
TagUtils.getInstance().write(pageContext, results.toString());
@@ -243,7 +248,7 @@ protected String renderFormStart() throws JspException {
results.append(" method=\"");
results.append(method == null ? "post" : method);
results.append("\" action=\"");
- results.append( response.encodeURL(context.toString()) );
+ results.append(response.encodeURL(appendSpringCsrfTokenToMultipartAction(context.toString(), request)));
results.append("\"");
@@ -286,6 +291,60 @@ protected String renderFormStart() throws JspException {
return results.toString();
}
+ protected String renderSpringCsrfToken(HttpServletRequest request) {
+ if (isSafeMethod()) {
+ return "";
+ }
+
+ CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
+ if (csrfToken == null) {
+ return "";
+ }
+
+ return "";
+ }
+
+ private String appendSpringCsrfTokenToMultipartAction(String actionUrl, HttpServletRequest request) {
+ if (isSafeMethod() || !isMultipartForm()) {
+ return actionUrl;
+ }
+
+ CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
+ if (csrfToken == null || hasQueryParameter(actionUrl, csrfToken.getParameterName())) {
+ return actionUrl;
+ }
+
+ String separator = actionUrl.indexOf('?') >= 0 ? "&" : "?";
+ return actionUrl + separator + urlEncode(csrfToken.getParameterName()) + "=" + urlEncode(csrfToken.getToken());
+ }
+
+ private boolean isMultipartForm() {
+ return enctype != null && "multipart/form-data".equalsIgnoreCase(enctype);
+ }
+
+ private boolean hasQueryParameter(String url, String parameterName) {
+ return url.contains("?" + parameterName + "=") || url.contains("&" + parameterName + "=");
+ }
+
+ private String urlEncode(String value) {
+ try {
+ return URLEncoder.encode(value, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private boolean isSafeMethod() {
+ return "get".equalsIgnoreCase(method)
+ || "head".equalsIgnoreCase(method)
+ || "options".equalsIgnoreCase(method)
+ || "trace".equalsIgnoreCase(method);
+ }
+
/**
* Release any acquired resources.
*/
diff --git a/amp/src/main/java/org/digijava/kernel/web/SecurityHeadersFilter.java b/amp/src/main/java/org/digijava/kernel/web/SecurityHeadersFilter.java
new file mode 100644
index 00000000000..7cf8d036478
--- /dev/null
+++ b/amp/src/main/java/org/digijava/kernel/web/SecurityHeadersFilter.java
@@ -0,0 +1,103 @@
+package org.digijava.kernel.web;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Servlet filter that adds browser-hardening HTTP security headers to every response.
+ *
+ * Headers applied:
+ *
+ * - {@code X-Content-Type-Options: nosniff} – prevents MIME-type sniffing.
+ * - {@code X-Frame-Options: SAMEORIGIN} – blocks cross-origin iframe embedding.
+ * - {@code X-XSS-Protection: 0} – disables the legacy browser XSS Auditor (which can
+ * introduce vulnerabilities of its own; modern browsers ignore it in favour of CSP).
+ * - {@code Referrer-Policy: strict-origin-when-cross-origin} – limits referrer
+ * leakage on cross-origin navigations.
+ * - {@code Content-Security-Policy} – restricts resource origins to reduce XSS and
+ * data-injection attack surface. {@code 'unsafe-inline'} and {@code 'unsafe-eval'}
+ * are permitted for scripts and styles because the legacy Wicket/Struts UI embeds
+ * inline code extensively; tightening these directives requires a separate
+ * front-end refactor.
+ * - {@code Strict-Transport-Security: max-age=31536000; includeSubDomains} – instructs
+ * browsers to use HTTPS exclusively for one year. Applied only when the current
+ * request itself arrived over TLS, so plain-HTTP local environments are not
+ * immediately broken. See the cookie policy note below.
+ *
+ *
+ * Cookie policy: the session cookie is declared {@code HttpOnly} and
+ * {@code Secure} through {@code } in {@code web.xml}.
+ * {@code HttpOnly} prevents client-side JavaScript from reading the session token.
+ * {@code Secure} ensures the cookie is only transmitted over HTTPS.
+ *
+ * HTTPS requirement: the {@code Secure} cookie flag and HSTS together require
+ * every deployment to terminate TLS. If the application sits behind a TLS-terminating
+ * load-balancer or reverse-proxy, configure it to forward {@code X-Forwarded-Proto:
+ * https}; Tomcat must then be configured with a {@code RemoteIpValve} (or equivalent)
+ * so that {@link HttpServletRequest#isSecure()} returns {@code true} and
+ * {@link javax.servlet.http.HttpServletRequest#getScheme()} returns {@code "https"}.
+ * For local development over plain HTTP, remove the {@code true} entry
+ * from the {@code } in {@code web.xml}.
+ */
+public class SecurityHeadersFilter implements Filter {
+
+ private static final String HSTS_VALUE = "max-age=31536000; includeSubDomains";
+
+ /**
+ * Content-Security-Policy directive applied to every response.
+ *
+ * Directive notes:
+ *
+ * - {@code frame-ancestors 'self'} is the CSP equivalent of {@code X-Frame-Options:
+ * SAMEORIGIN} and takes precedence in browsers that support CSP Level 2.
+ * - {@code img-src 'self' data: blob:} is required because several UI components
+ * embed images as data URIs or object URLs.
+ * - {@code font-src 'self' data:} covers web-fonts bundled as data URIs.
+ *
+ */
+ private static final String CSP_VALUE =
+ "default-src 'self'; "
+ + "script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
+ + "style-src 'self' 'unsafe-inline'; "
+ + "img-src 'self' data: blob:; "
+ + "font-src 'self' data:; "
+ + "connect-src 'self'; "
+ + "frame-ancestors 'self'; "
+ + "object-src 'none';";
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+ httpResponse.setHeader("X-Content-Type-Options", "nosniff");
+ httpResponse.setHeader("X-Frame-Options", "SAMEORIGIN");
+ httpResponse.setHeader("X-XSS-Protection", "0");
+ httpResponse.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
+ httpResponse.setHeader("Content-Security-Policy", CSP_VALUE);
+
+ // Only add HSTS when the connection is already secure; sending it over plain
+ // HTTP would lock out users who cannot reach the HTTPS endpoint yet.
+ if (((HttpServletRequest) request).isSecure()) {
+ httpResponse.setHeader("Strict-Transport-Security", HSTS_VALUE);
+ }
+
+ chain.doFilter(request, response);
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git a/amp/src/main/webapp/WEB-INF/applicationContext.xml b/amp/src/main/webapp/WEB-INF/applicationContext.xml
index 451b5dc129d..ef47db8396f 100644
--- a/amp/src/main/webapp/WEB-INF/applicationContext.xml
+++ b/amp/src/main/webapp/WEB-INF/applicationContext.xml
@@ -18,6 +18,8 @@
+
+
@@ -182,7 +184,7 @@
-
+
@@ -201,22 +203,22 @@
-
+
-
+
-
+
-
+
@@ -250,7 +252,7 @@
-
+
diff --git a/amp/src/main/webapp/WEB-INF/jsp/aim/view/autologin.jsp b/amp/src/main/webapp/WEB-INF/jsp/aim/view/autologin.jsp
index 12b59038d4f..ee0b67888cd 100644
--- a/amp/src/main/webapp/WEB-INF/jsp/aim/view/autologin.jsp
+++ b/amp/src/main/webapp/WEB-INF/jsp/aim/view/autologin.jsp
@@ -12,6 +12,7 @@ function delayer(){