Skip to content
This repository was archived by the owner on Jan 15, 2020. It is now read-only.

Commit 20bcb4b

Browse files
committed
CLOUDSTACK-7063, CLOUDSTACK-7064: Add security headers on HTTP response
- Adds X-XSS-Protection header - Adds X-Content-Type-Options header - Fixes to use json content type defined from global settings - Uses secure cookie if enabled in global settings Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com> (cherry picked from commit b6b3494) Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
1 parent 843c0f8 commit 20bcb4b

4 files changed

Lines changed: 66 additions & 20 deletions

File tree

server/src/com/cloud/api/ApiServer.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
181181
private static final Logger s_accessLogger = Logger.getLogger("apiserver." + ApiServer.class.getName());
182182

183183
public static boolean encodeApiResponse = false;
184-
public static String jsonContentType = "text/javascript";
184+
public static boolean s_enableSecureCookie = false;
185+
public static String s_jsonContentType = HttpUtils.JSON_CONTENT_TYPE;
185186

186187
/**
187188
* Non-printable ASCII characters - numbers 0 to 31 and 127 decimal
@@ -362,9 +363,13 @@ public boolean start() {
362363
}
363364

364365
setEncodeApiResponse(Boolean.valueOf(_configDao.getValue(Config.EncodeApiResponse.key())));
365-
final String jsonType = _configDao.getValue(Config.JavaScriptDefaultContentType.key());
366+
final String jsonType = _configDao.getValue(Config.JSONDefaultContentType.key());
366367
if (jsonType != null) {
367-
jsonContentType = jsonType;
368+
s_jsonContentType = jsonType;
369+
}
370+
final Boolean enableSecureSessionCookie = Boolean.valueOf(_configDao.getValue(Config.EnableSecureSessionCookie.key()));
371+
if (enableSecureSessionCookie != null) {
372+
s_enableSecureCookie = enableSecureSessionCookie;
368373
}
369374

370375
if (apiPort != null) {
@@ -1136,7 +1141,7 @@ private void writeResponse(final HttpResponse resp, final String responseText, f
11361141
final BasicHttpEntity body = new BasicHttpEntity();
11371142
if (HttpUtils.RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) {
11381143
// JSON response
1139-
body.setContentType(jsonContentType);
1144+
body.setContentType(getJSONContentType());
11401145
if (responseText == null) {
11411146
body.setContent(new ByteArrayInputStream("{ \"error\" : { \"description\" : \"Internal Server Error\" } }".getBytes(HttpUtils.UTF_8)));
11421147
}
@@ -1367,7 +1372,11 @@ private static void setEncodeApiResponse(final boolean encodeApiResponse) {
13671372
ApiServer.encodeApiResponse = encodeApiResponse;
13681373
}
13691374

1370-
public static String getJsonContentType() {
1371-
return jsonContentType;
1375+
public static boolean isSecureSessionCookieEnabled() {
1376+
return s_enableSecureCookie;
1377+
}
1378+
1379+
public static String getJSONContentType() {
1380+
return s_jsonContentType;
13721381
}
13731382
}

server/src/com/cloud/api/ApiServlet.java

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,20 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp
155155
try {
156156

157157
if (HttpUtils.RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) {
158-
resp.setContentType(HttpUtils.JSON_CONTENT_TYPE);
158+
resp.setContentType(ApiServer.getJSONContentType());
159159
} else if (HttpUtils.RESPONSE_TYPE_XML.equalsIgnoreCase(responseType)){
160160
resp.setContentType(HttpUtils.XML_CONTENT_TYPE);
161161
}
162162

163163
HttpSession session = req.getSession(false);
164+
if (ApiServer.isSecureSessionCookieEnabled()) {
165+
resp.setHeader("SET-COOKIE", "JSESSIONID=" + session.getId() + ";Secure;Path=/client");
166+
if (s_logger.isDebugEnabled()) {
167+
if (s_logger.isDebugEnabled()) {
168+
s_logger.debug("Session cookie is marked secure!");
169+
}
170+
}
171+
}
164172
final Object[] responseTypeParam = params.get(ApiConstants.RESPONSE);
165173
if (responseTypeParam != null) {
166174
responseType = (String)responseTypeParam[0];
@@ -213,7 +221,7 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp
213221
}
214222
}
215223
}
216-
HttpUtils.writeHttpResponse(resp, responseString, httpResponseCode, responseType);
224+
HttpUtils.writeHttpResponse(resp, responseString, httpResponseCode, responseType, ApiServer.getJSONContentType());
217225
return;
218226
}
219227
}
@@ -240,7 +248,7 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp
240248
auditTrailSb.append(" " + HttpServletResponse.SC_UNAUTHORIZED + " " + "unable to verify user credentials");
241249
final String serializedResponse =
242250
_apiServer.getSerializedApiError(HttpServletResponse.SC_UNAUTHORIZED, "unable to verify user credentials", params, responseType);
243-
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType);
251+
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType, ApiServer.getJSONContentType());
244252
return;
245253
}
246254

@@ -251,7 +259,7 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp
251259
s_logger.info("missing command, ignoring request...");
252260
auditTrailSb.append(" " + HttpServletResponse.SC_BAD_REQUEST + " " + "no command specified");
253261
final String serializedResponse = _apiServer.getSerializedApiError(HttpServletResponse.SC_BAD_REQUEST, "no command specified", params, responseType);
254-
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_BAD_REQUEST, responseType);
262+
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_BAD_REQUEST, responseType, ApiServer.getJSONContentType());
255263
return;
256264
}
257265
final User user = _entityMgr.findById(User.class, userId);
@@ -267,7 +275,7 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp
267275
auditTrailSb.append(" " + HttpServletResponse.SC_UNAUTHORIZED + " " + "unable to verify user credentials");
268276
final String serializedResponse =
269277
_apiServer.getSerializedApiError(HttpServletResponse.SC_UNAUTHORIZED, "unable to verify user credentials", params, responseType);
270-
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType);
278+
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType, ApiServer.getJSONContentType());
271279
return;
272280
}
273281
} else {
@@ -281,7 +289,7 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp
281289
// Add the HTTP method (GET/POST/PUT/DELETE) as well into the params map.
282290
params.put("httpmethod", new String[] {req.getMethod()});
283291
final String response = _apiServer.handleRequest(params, responseType, auditTrailSb);
284-
HttpUtils.writeHttpResponse(resp, response != null ? response : "", HttpServletResponse.SC_OK, responseType);
292+
HttpUtils.writeHttpResponse(resp, response != null ? response : "", HttpServletResponse.SC_OK, responseType, ApiServer.getJSONContentType());
285293
} else {
286294
if (session != null) {
287295
try {
@@ -294,13 +302,13 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp
294302
final String serializedResponse =
295303
_apiServer.getSerializedApiError(HttpServletResponse.SC_UNAUTHORIZED, "unable to verify user credentials and/or request signature", params,
296304
responseType);
297-
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType);
305+
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType, ApiServer.getJSONContentType());
298306

299307
}
300308
} catch (final ServerApiException se) {
301309
final String serializedResponseText = _apiServer.getSerializedApiError(se, params, responseType);
302310
resp.setHeader("X-Description", se.getDescription());
303-
HttpUtils.writeHttpResponse(resp, serializedResponseText, se.getErrorCode().getHttpCode(), responseType);
311+
HttpUtils.writeHttpResponse(resp, serializedResponseText, se.getErrorCode().getHttpCode(), responseType, ApiServer.getJSONContentType());
304312
auditTrailSb.append(" " + se.getErrorCode() + " " + se.getDescription());
305313
} catch (final Exception ex) {
306314
s_logger.error("unknown exception writing api response", ex);

server/src/com/cloud/configuration/Config.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,13 +1592,22 @@ public enum Config {
15921592
"Percentage (as a value between 0 and 1) of connected agents after which agent load balancing will start happening",
15931593
null),
15941594

1595-
JavaScriptDefaultContentType(
1595+
JSONDefaultContentType(
15961596
"Advanced",
15971597
ManagementServer.class,
15981598
String.class,
15991599
"json.content.type",
1600-
"text/javascript",
1601-
"Http response content type for .js files (default is text/javascript)",
1600+
"application/json; charset=UTF-8",
1601+
"Http response content type for JSON",
1602+
null),
1603+
1604+
EnableSecureSessionCookie(
1605+
"Advanced",
1606+
ManagementServer.class,
1607+
Boolean.class,
1608+
"enable.secure.session.cookie",
1609+
"false",
1610+
"Session cookie's secure flag is enabled if true. Use this only when using HTTPS",
16021611
null),
16031612

16041613
DefaultMaxDomainUserVms("Domain Defaults", ManagementServer.class, Long.class, "max.domain.user.vms", "40", "The default maximum number of user VMs that can be deployed for a domain", null),

utils/src/com/cloud/utils/HttpUtils.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,40 @@ public class HttpUtils {
3131
public static final String UTF_8 = "UTF-8";
3232
public static final String RESPONSE_TYPE_JSON = "json";
3333
public static final String RESPONSE_TYPE_XML = "xml";
34-
public static final String JSON_CONTENT_TYPE = "text/javascript; charset=UTF-8";
34+
public static final String JSON_CONTENT_TYPE = "application/json; charset=UTF-8";
3535
public static final String XML_CONTENT_TYPE = "text/xml; charset=UTF-8";
3636

37+
public static void addSecurityHeaders(final HttpServletResponse resp) {
38+
if (resp.containsHeader("X-Content-Type-Options")) {
39+
resp.setHeader("X-Content-Type-Options", "nosniff");
40+
}
41+
else {
42+
resp.addHeader("X-Content-Type-Options", "nosniff");
43+
}
44+
if (resp.containsHeader("X-XSS-Protection")) {
45+
resp.setHeader("X-XSS-Protection", "1;mode=block");
46+
}
47+
else {
48+
resp.addHeader("X-XSS-Protection", "1;mode=block");
49+
}
50+
}
51+
3752
public static void writeHttpResponse(final HttpServletResponse resp, final String response,
38-
final Integer responseCode, final String responseType) {
53+
final Integer responseCode, final String responseType, final String jsonContentType) {
3954
try {
4055
if (RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) {
41-
resp.setContentType(JSON_CONTENT_TYPE);
56+
if (jsonContentType != null && !jsonContentType.isEmpty()) {
57+
resp.setContentType(jsonContentType);
58+
} else {
59+
resp.setContentType(JSON_CONTENT_TYPE);
60+
}
4261
} else if (RESPONSE_TYPE_XML.equalsIgnoreCase(responseType)){
4362
resp.setContentType(XML_CONTENT_TYPE);
4463
}
4564
if (responseCode != null) {
4665
resp.setStatus(responseCode);
4766
}
67+
addSecurityHeaders(resp);
4868
resp.getWriter().print(response);
4969
} catch (final IOException ioex) {
5070
if (s_logger.isTraceEnabled()) {

0 commit comments

Comments
 (0)