![JAR search and dependency download from the Maven repository](/logo.png)
de.tsl2.nano.h5.WebSecurity Maven / Gradle / Ivy
package de.tsl2.nano.h5;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import de.tsl2.nano.core.ENV;
import de.tsl2.nano.core.ManagedException;
import de.tsl2.nano.core.secure.Crypt;
import de.tsl2.nano.core.util.DateUtil;
import de.tsl2.nano.core.util.MapUtil;
import de.tsl2.nano.core.util.StringUtil;
import de.tsl2.nano.h5.NanoHTTPD.Response;
/**
* tries to resolve simple OWASP security aspects.
* asks ENV for properties starting with "app.session."
*
* see: https://cheatsheetseries.owasp.org/IndexTopTen.html
* see: https://infosec.mozilla.org/guidelines/web_security#cross-origin-resource-sharing
*
* @author ts
*/
public class WebSecurity {
private String antiCSRFKey;
private static final String ENV_PREF = "app.session.";
private static final String PREF_ANTICSRF = ENV_PREF + "anticsrf";
public static final String CSRF_TOKEN = "csrftoken";
public static final String DEF_ALG = "AES";
private static final String ETAG = "ETag";
private static final String REQUEST_COOKIE = "cookie";
private static final String SET_COOKIE = "Set-Cookie";
private static final String SEP = "---";
private static final String SESSION_ID = "session-id";
private static final String STANDARD_HEADER =
"""
Referrer-Policy: same-origin
X-XSS-Protection: 1;mode=block
X-Permitted-Cross-Domain-Policies: master-only
X-Frame-Options: sameorigin
Content-Security-Policy: default-src 'self';
X-Content-Type-Options: nosniff;
Strict-Transport-Security: maxage=31536000;
IncludeSubDomains: true;
Content-Security-Policy: script-src 'self' 'unsafe-inline' 'XnXoXnXcXe-${requestId}';
Content-Security-Policy: frame-src 'self';
Content-Security-Policy: frame-ancestors 'self';
Content-Security-Policy: form-action 'self';
Content-Security-Policy: default-src 'self' 'unsafe-inline' filesystem ${service.url} ${websocket.url};
""";
public static boolean useAntiCSRFToken() {
return ENV.get(PREF_ANTICSRF, true);
}
public static boolean useAntiCSRFTokenInContent() {
return useAntiCSRFToken() && ENV.get(PREF_ANTICSRF + ".incontent", true);
}
public static boolean useAntiCSRFTokenInHeader() {
return useAntiCSRFToken() && ENV.get(PREF_ANTICSRF + ".inheader", true);
}
public String createAntiCSRFToken(NanoH5Session session) {
try {
return Crypt.encrypt(session.getKey() + SEP
+ (session.getWorkingObject() != null ? session.getWorkingObject().getId() : "NOTHING") + SEP
+ System.currentTimeMillis(), getAntiCSRFKey(), ENV.get(PREF_ANTICSRF + ".algorithm", DEF_ALG));
} catch (Exception e) {
if (session != null)
session.close();
ManagedException.forward(e);
return null;
}
}
private String getAntiCSRFKey() {
if (antiCSRFKey == null)
antiCSRFKey = Crypt.generatePassword((byte)16);
return antiCSRFKey;
}
private void checkAntCSRFToken(NanoH5Session session, String token) {
if (!useAntiCSRFToken())
return;
if (token == null)
throw new IllegalStateException("request is missing anti-csrf token");
String sessionInfo = Crypt.decrypt(token, getAntiCSRFKey(), ENV.get(PREF_ANTICSRF + ".algorithm", DEF_ALG));
String[] splitInfo = sessionInfo.split("[-]{3}");
boolean attack = false;
if (splitInfo[0].equals(session.getKey())) {
Date now = new Date();
Date tokenAge = new Date(Long.valueOf(splitInfo[2]) + ENV.get(PREF_ANTICSRF + ".maxage.milliseconds", 3600*1000));
if (tokenAge.before(now))
attack = true;
else {
if (ENV.get(PREF_ANTICSRF + ".check.request", true)) {
if (session.getWorkingObject() != null) // session closed directly before
if (!splitInfo[1].equals(String.valueOf(session.getWorkingObject().getId())))
attack = true;
}
}
}
else
attack = true;
if (attack) {
session.close();
throw new IllegalStateException("request outdated or unauthorized! closing session!");
}
}
public void checkSession(NanoH5Session session, String method, Map header, Map params) {
if (session.isNew())
return;
Map sessionValues = getCookieValues(header);
try {
checkSessionID(session, sessionValues);
if (session.getUserAuthorization() != null) {
if (useAntiCSRFTokenInHeader())
checkAntCSRFToken(session, sessionValues.get(CSRF_TOKEN));
if (method.equals("POST") && useAntiCSRFTokenInContent())
checkAntCSRFToken(session, params.get(CSRF_TOKEN));
}
} catch (Exception ex) {
session.close();
ManagedException.forward(ex);
}
}
private Map getCookieValues(Map header) {
String sessionTag = header.get(REQUEST_COOKIE);
String[] hs = sessionTag.split("[;]");
Map sessionValues = new LinkedHashMap<>(hs.length);
Arrays.stream(hs).forEach(e -> MapUtil.add(sessionValues, e.trim().split("\\s*=\\s*")));
return sessionValues;
}
private void checkSessionID(NanoH5Session session, Map sessionValues) {
String sessionId = sessionValues.get(SESSION_ID);
if (sessionId == null)
throw new IllegalStateException("missing " + SESSION_ID);
if (!sessionId.equals(session.getKey()))
throw new IllegalStateException("bad " + SESSION_ID);
}
public Response addSessionHeader(NanoH5Session session, Response response) {
if (session != null) {
addSessionID(session, response);
if (session.getUserAuthorization() != null && useAntiCSRFTokenInHeader()) {
response.addHeader(getSessionTagName(), CSRF_TOKEN + "=" + createAntiCSRFToken(session));
}
}
Properties p = provideProperties(session);
String header = getStandardHeader();
String[] keyValues = header.split("\n");
String k, v, kk = "", vv = "";
for (int i = 0; i < keyValues.length; i++) {
k = StringUtil.substring(keyValues[i], null, ":").trim();
v = (k.equals(kk) ? vv + " ": "") + StringUtil.substring(keyValues[i], ":",null).trim();
v = StringUtil.insertProperties(v, p);
if (k.trim().length() > 0 && v.trim().length() > 0)
response.addHeader(k, v);
kk = k;
vv = v;
}
return response;
}
private Properties provideProperties(NanoH5Session session) {
Properties p = new Properties(System.getProperties());
String serviceUrl = ENV.get("service.url", "");
String websocketUrl = session != null ? StringUtil.substring(serviceUrl.replace("http", "ws"), null, ":", true)
+ ":" + session.getWebsocketPort() : "";
p.put("service.url", serviceUrl);
p.put("websocket.url", websocketUrl);
p.put("requestId", session != null ? session.getRequestId() : "");
return p;
}
private String getStandardHeader() {
String header = ENV.get(ENV_PREF + "httpheader",
STANDARD_HEADER);
return header;
}
public static Object getSessionID(Map header, InetAddress requestor) {
//header keys are case insenstive and are stored by NanoHttpd in lower case!
return header.containsKey("if-none-match") ? header.get("if-none-match")
: header.containsKey(REQUEST_COOKIE) ? StringUtil.substring(header.get(REQUEST_COOKIE), SESSION_ID + "=", ";")
: requestor;
}
protected void addSessionID(NanoH5Session session, Response response) {
String sessionMode = getSessionTagName();
String maxAge = getMaxAge();
if (sessionMode.equals(SET_COOKIE)) {
String secure = ENV.get("app.ssl.activate", false) ? "secure; " : ";";
response.addHeader(SET_COOKIE, SESSION_ID + "=" + session.getKey() + ";" + secure + maxAge
+ ENV.get(ENV_PREF + "cookie.parameter", "SameSite=Strict; HttpOnly; Path=/"));
} else if (sessionMode.equals(ETAG)) {
addETag(session.getKey(), response, maxAge);
} else { // client-ip
}
}
private String getMaxAge() {
return "Max-Age=" + (ENV.get(ENV_PREF + "timeout.millis", 30 * DateUtil.T_MINUTE) / 1000) + ";";
}
private String getSessionTagName() {
return ENV.get(ENV_PREF + "tag", SET_COOKIE);
}
public void addETag(String name, Response response, String maxAge) {
response.addHeader(ETAG, "\"" + name + "\"");
// response.addHeader("Vary", "User-Agent");
response.addHeader("Cache-Control",maxAge);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy