This site is the archived OWASP Foundation Wiki and is no longer accepting Account Requests.
To view the new OWASP Foundation website, please visit https://owasp.org
HTML5 Security Cheat Sheet
Last revision (mm/dd/yy): 08/30/2017 IntroductionThe following cheat sheet serves as a guide for implementing HTML 5 in a secure fashion. Communication APIsWeb MessagingWeb Messaging (also known as Cross Domain Messaging) provides a means of messaging between documents from different origins in a way that is generally safer than the multiple hacks used in the past to accomplish this task. However, there are still some recommendations to keep in mind:
Cross Origin Resource Sharing
WebSockets
Server-Sent Events
Storage APIsLocal Storage
Client-side databases
Geolocation
Web Workers
Sandboxed frames
It is possible to have a fine-grained control over iframe capabilities using the value of the sandbox attribute.
Offline Applications
Progressive Enhancements and Graceful Degradation Risks
HTTP Headers to enhance securityX-Frame-Options
X-XSS-Protection
Strict Transport Security
Content Security Policy
Origin
WebSocket implementation hintsIn addition to the elements mentioned above, this is the list of area for which caution must be taken during the implementation.
The section below will propose some implementation hints on every area and will be along with an application example showing all the points described. The complete source code of the example application is available here. Access filteringDuring a websocket channel initiation, the browser send the Origin HTTP request header that contain th source domain initiation the request to handshake. Event if this header can be spoofed in a forged HTTP request (not browser based), it cannot be overrided or forced in a browser context. It then represent a good candidate to apply filtering according to an expected value. An example of attack using this vector and named Cross-Site WebSocket Hijacking (CSWSH) is described here. The code below define a configuration that apply filtering based on a "whitelist" of origins. This ensure that only allowed origins can establish a full handshake: import org.owasp.encoder.Encode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.websocket.server.ServerEndpointConfig; import java.util.Arrays; import java.util.List; /** * Setup handshake rules applied to all WebSocket endpoints of the application. * Use to setup the Access Filtering using "Origin" HTTP header as input information. * * @see "http://docs.oracle.com/javaee/7/api/index.html?javax/websocket/server/ServerEndpointConfig.Configurator.html" * @see "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin" */ public class EndpointConfigurator extends ServerEndpointConfig.Configurator { /** * Logger */ private static final Logger LOG = LoggerFactory.getLogger(EndpointConfigurator.class); /** * Get the expected source origins from a JVM property in order to allow external configuration */ private static final List<String> EXPECTED_ORIGINS = Arrays.asList(System.getProperty("source.origins").split(";")); /** * {@inheritDoc} */ @Override public boolean checkOrigin(String originHeaderValue) { boolean isAllowed = EXPECTED_ORIGINS.contains(originHeaderValue); String safeOriginValue = Encode.forHtmlContent(originHeaderValue); if (isAllowed) { LOG.info("[EndpointConfigurator] New handshake request received from {} and was accepted.", safeOriginValue); } else { LOG.warn("[EndpointConfigurator] New handshake request received from {} and was rejected !", safeOriginValue); } return isAllowed; } } Authentication and Input/Output validationWhen using websocket as communication channel, it's important to use an authentication method allowing the user to receive an access Token that is not automatically sent by the browser and then must be expliclty sent by the client code during each exchange. JSON Web Token is a good candidate because it allow to transport access ticket information in a stateless and not alterable way. Moreover, it define a validity timeframe. You can find additional information about JWT token hardening on this article. JSON Validation Schema are used to define and validate the expected content in input and ouput messages. The code below define the complete authentication messages flow handling: Authentication Web Socket endpoint - Provide a WS endpoint the enable authentication exchange import org.owasp.pocwebsocket.configurator.EndpointConfigurator; import org.owasp.pocwebsocket.decoder.AuthenticationRequestDecoder; import org.owasp.pocwebsocket.encoder.AuthenticationResponseEncoder; import org.owasp.pocwebsocket.handler.AuthenticationMessageHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.websocket.CloseReason; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; /** * Class in charge of managing the client authentication. * * @see "http://docs.oracle.com/javaee/7/api/javax/websocket/server/ServerEndpointConfig.Configurator.html" * @see "http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/" */ @ServerEndpoint(value = "/auth", configurator = EndpointConfigurator.class, subprotocols = {"authentication"}, encoders = {AuthenticationResponseEncoder.class}, decoders = {AuthenticationRequestDecoder.class}) public class AuthenticationEndpoint { /** * Logger */ private static final Logger LOG = LoggerFactory.getLogger(AuthenticationEndpoint.class); /** * Handle the beginning of an exchange * * @param session Exchange session information */ @OnOpen public void start(Session session) { //Define connection idle timeout and message limits in order to mitigate as much as possible DOS attacks using massive connection opening or massive big messages sending int msgMaxSize = 1024 * 1024;//1 MB session.setMaxIdleTimeout(60000);//1 minute session.setMaxTextMessageBufferSize(msgMaxSize); session.setMaxBinaryMessageBufferSize(msgMaxSize); //Log exchange start LOG.info("[AuthenticationEndpoint] Session {} started", session.getId()); //Affect a new message handler instance in order to process the exchange session.addMessageHandler(new AuthenticationMessageHandler(session.getBasicRemote())); LOG.info("[AuthenticationEndpoint] Session {} message handler affected for processing", session.getId()); } /** * Handle error case * * @param session Exchange session information * @param thr Error details */ @OnError public void onError(Session session, Throwable thr) { LOG.error("[AuthenticationEndpoint] Error occur in session {}", session.getId(), thr); } /** * Handle close event * * @param session Exchange session information * @param closeReason Exchange closing reason */ @OnClose public void onClose(Session session, CloseReason closeReason) { LOG.info("[AuthenticationEndpoint] Session {} closed: {}", session.getId(), closeReason.getReasonPhrase()); } }
import org.owasp.pocwebsocket.enumeration.AccessLevel; import org.owasp.pocwebsocket.util.AuthenticationUtils; import org.owasp.pocwebsocket.vo.AuthenticationRequest; import org.owasp.pocwebsocket.vo.AuthenticationResponse; import org.owasp.encoder.Encode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.websocket.EncodeException; import javax.websocket.MessageHandler; import javax.websocket.RemoteEndpoint; import java.io.IOException; /** * Handle authentication message flow */ public class AuthenticationMessageHandler implements MessageHandler.Whole<AuthenticationRequest> { private static final Logger LOG = LoggerFactory.getLogger(AuthenticationMessageHandler.class); /** * Reference to the communication channel with the client */ private RemoteEndpoint.Basic clientConnection; /** * Constructor * * @param clientConnection Reference to the communication channel with the client */ public AuthenticationMessageHandler(RemoteEndpoint.Basic clientConnection) { this.clientConnection = clientConnection; } /** * {@inheritDoc} */ @Override public void onMessage(AuthenticationRequest message) { AuthenticationResponse response = null; try { //Authenticate String authenticationToken = ""; String accessLevel = this.authenticate(message.getLogin(), message.getPassword()); if (accessLevel != null) { //Create a simple JSON token representing the authentication profile authenticationToken = AuthenticationUtils.issueToken(message.getLogin(), accessLevel); } //Build the response object String safeLoginValue = Encode.forHtmlContent(message.getLogin()); if (!authenticationToken.isEmpty()) { response = new AuthenticationResponse(true, authenticationToken, "Authentication succeed !"); LOG.info("[AuthenticationMessageHandler] User {} authentication succeed.", safeLoginValue); } else { response = new AuthenticationResponse(false, authenticationToken, "Authentication failed !"); LOG.warn("[AuthenticationMessageHandler] User {} authentication failed.", safeLoginValue); } } catch (Exception e) { LOG.error("[AuthenticationMessageHandler] Error occur in authentication process.", e); //Build the response object indicating that authentication fail response = new AuthenticationResponse(false, "", "Authentication failed !"); } finally { //Send response try { this.clientConnection.sendObject(response); } catch (IOException | EncodeException e) { LOG.error("[AuthenticationMessageHandler] Error occur in response object sending.", e); } } } /** * Authenticate the user * * @param login User login * @param password User password * @return The access level if the authentication succeed or NULL if the authentication failed */ private String authenticate(String login, String password) { .... } }
import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Calendar; import java.util.Locale; /** * Utility class to manage the authentication JWT token */ public class AuthenticationUtils { /** * Build a JWT token for a user * * @param login User login * @param accessLevel Access level of the user * @return The Base64 encoded JWT token * @throws Exception If any error occur during the issuing */ public static String issueToken(String login, String accessLevel) throws Exception { //Issue a JWT token with validity of 30 minutes Algorithm algorithm = Algorithm.HMAC256(loadSecret()); Calendar c = Calendar.getInstance(); c.add(Calendar.MINUTE, 30); return JWT.create().withIssuer("WEBSOCKET-SERVER").withSubject(login).withExpiresAt(c.getTime()).withClaim("access_level", accessLevel.trim().toUpperCase(Locale.US)).sign(algorithm); } /** * Verify the validity of the provided JWT token * * @param token JWT token encoded to verify * @return The verified and decoded token with user authentication and authorization (access level) information * @throws Exception If any error occur during the token validation */ public static DecodedJWT validateToken(String token) throws Exception { Algorithm algorithm = Algorithm.HMAC256(loadSecret()); JWTVerifier verifier = JWT.require(algorithm).withIssuer("WEBSOCKET-SERVER").build(); return verifier.verify(token); } /** * Load the JWT secret used to sign token using a byte array for secret storage in order to avoid persistent string in memory * * @return The secret as byte array * @throws IOException If any error occur during the secret loading */ private static byte[] loadSecret() throws IOException { return Files.readAllBytes(Paths.get("src", "main", "resources", "jwt-secret.txt")); } }
{ "$schema": "http://json-schema.org/schema#", "title": "AuthenticationRequest", "type": "object", "properties": { "login": { "type": "string", "pattern": "^[a-zA-Z]{1,10}$" }, "password": { "type": "string" } }, "required": [ "login", "password" ] } { "$schema": "http://json-schema.org/schema#", "title": "AuthenticationResponse", "type": "object", "properties": { "isSuccess;": { "type": "boolean" }, "token": { "type": "string", "pattern": "^[a-zA-Z0-9+/=\\._-]{0,500}$" }, "message": { "type": "string", "pattern": "^[a-zA-Z0-9!\\s]{0,100}$" } }, "required": [ "isSuccess", "token", "message" ] }
import com.fasterxml.jackson.databind.JsonNode; import com.github.fge.jackson.JsonLoader; import com.github.fge.jsonschema.core.exceptions.ProcessingException; import com.github.fge.jsonschema.core.report.ProcessingReport; import com.github.fge.jsonschema.main.JsonSchema; import com.github.fge.jsonschema.main.JsonSchemaFactory; import com.google.gson.Gson; import org.owasp.pocwebsocket.vo.AuthenticationRequest; import javax.websocket.DecodeException; import javax.websocket.Decoder; import javax.websocket.EndpointConfig; import java.io.File; import java.io.IOException; /** * Decode JSON text representation to an AuthenticationRequest object * <p> * As there one instance of the decoder class by endpoint session so we can use the JsonSchema as decoder instance variable. */ public class AuthenticationRequestDecoder implements Decoder.Text<AuthenticationRequest> { /** * JSON validation schema associated to this type of message */ private JsonSchema validationSchema = null; /** * Initialize decoder and associated JSON validation schema * * @throws IOException If any error occur during the object creation * @throws ProcessingException If any error occur during the schema loading */ public AuthenticationRequestDecoder() throws IOException, ProcessingException { JsonNode node = JsonLoader.fromFile(new File("src/main/resources/authentication-request-schema.json")); this.validationSchema = JsonSchemaFactory.byDefault().getJsonSchema(node); } /** * {@inheritDoc} */ @Override public AuthenticationRequest decode(String s) throws DecodeException { try { //Validate the provided representation against the dedicated schema //Use validation mode with report in order to enable further inspection/tracing of the error details //Moreover the validation method "validInstance()" generate a NullPointerException if the representation do not respect the expected schema //so it's more proper to use the validation method with report ProcessingReport validationReport = this.validationSchema.validate(JsonLoader.fromString(s), true); //Ensure there no error if (!validationReport.isSuccess()) { //Simply reject the message here: Don't care about error details... throw new DecodeException(s, "Validation of the provided representation failed !"); } } catch (IOException | ProcessingException e) { throw new DecodeException(s, "Cannot validate the provided representation to a JSON valid representation !", e); } return new Gson().fromJson(s, AuthenticationRequest.class); } /** * {@inheritDoc} */ @Override public boolean willDecode(String s) { boolean canDecode = false; //If the provided JSON representation is empty/null then we indicate that representation cannot be decoded to our expected object if (s == null || s.trim().isEmpty()) { return canDecode; } //Try to cast the provided JSON representation to our object to validate at least the structure (content validation is done during decoding) try { AuthenticationRequest test = new Gson().fromJson(s, AuthenticationRequest.class); canDecode = (test != null); } catch (Exception e) { //Ignore explicitly any casting error... } return canDecode; } /** * {@inheritDoc} */ @Override public void init(EndpointConfig config) { //Not used } /** * {@inheritDoc} */ @Override public void destroy() { //Not used } } import com.fasterxml.jackson.databind.JsonNode; import com.github.fge.jackson.JsonLoader; import com.github.fge.jsonschema.core.exceptions.ProcessingException; import com.github.fge.jsonschema.core.report.ProcessingReport; import com.github.fge.jsonschema.main.JsonSchema; import com.github.fge.jsonschema.main.JsonSchemaFactory; import com.google.gson.Gson; import org.owasp.pocwebsocket.vo.AuthenticationResponse; import javax.websocket.EncodeException; import javax.websocket.Encoder; import javax.websocket.EndpointConfig; import java.io.File; import java.io.IOException; /** * Encode AuthenticationResponse object to JSON text representation. * <p> * As there one instance of the encoder class by endpoint session so we can use the JsonSchema as encoder instance variable. */ public class AuthenticationResponseEncoder implements Encoder.Text<AuthenticationResponse> { /** * JSON validation schema associated to this type of message */ private JsonSchema validationSchema = null; /** * Initialize encoder and associated JSON validation schema * * @throws IOException If any error occur during the object creation * @throws ProcessingException If any error occur during the schema loading */ public AuthenticationResponseEncoder() throws IOException, ProcessingException { JsonNode node = JsonLoader.fromFile(new File("src/main/resources/authentication-response-schema.json")); this.validationSchema = JsonSchemaFactory.byDefault().getJsonSchema(node); } /** * {@inheritDoc} */ @Override public String encode(AuthenticationResponse object) throws EncodeException { //Generate the JSON representation String json = new Gson().toJson(object); try { //Validate the generated representation against the dedicated schema //Use validation mode with report in order to enable further inspection/tracing of the error details //Moreover the validation method "validInstance()" generate a NullPointerException if the representation do not respect the expected schema //so it's more proper to use the validation method with report ProcessingReport validationReport = this.validationSchema.validate(JsonLoader.fromString(json), true); //Ensure there no error if (!validationReport.isSuccess()) { //Simply reject the message here: Don't care about error details... throw new EncodeException(object, "Validation of the generated representation failed !"); } } catch (IOException | ProcessingException e) { throw new EncodeException(object, "Cannot validate the generated representation to a JSON valid representation !", e); } return json; } /** * {@inheritDoc} */ @Override public void init(EndpointConfig config) { //Not used } /** * {@inheritDoc} */ @Override public void destroy() { //Not used } } Note that the same approach is used in the messages handling part of the POC. All messages exchanged between the client and the server are systematically validated using the same way, using dedicated JSON schemas linked to messages dedicated Encoder/Decoder (serialization/deserialization). Authorization and access token explicit invalidationAuthorization information is stored in the access token using the JWT Claim feature (in the POC the name of the claim is access_level). Authorization is validated when a request is received and before any other action using the user input information. The access token is passed with every message sent to the message endpoint and a blacklist is used in order to allow the user to request an explicit token invalidation. Explicit token invalidation is interesting from a user point of view because, often when token are used, the validity timeframe of the token is relatively long (it's common to see a valid timeframe superior to 1 hour) so it's important to allow a user to have a way to indicate to the system "OK, i have finished my exchange with you so you can close our exchange session and cleanup associated links". It's help also a user to revoke itself is current access if a malicious concurrent access is detected using the same token (case of token stealing). Token blacklist - Maintain a temporary list using memory and time limited Caching of hashes of token that are not allowed to be used anymore import org.apache.commons.jcs.JCS; import org.apache.commons.jcs.access.CacheAccess; import org.apache.commons.jcs.access.exception.CacheException; import javax.xml.bind.DatatypeConverter; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * Utility class to manage the access token that have been declared as no more usable (explicit user logout) */ public class AccessTokenBlacklistUtils { /** * Message content send by user that indicate that the access token that come along the message must be blacklisted for further usage */ public static final String MESSAGE_ACCESS_TOKEN_INVALIDATION_FLAG = "INVALIDATE_TOKEN"; /** * Use cache to store blacklisted token hash in order to avoid memory exhaustion and be consistent because token are valid 30 minutes so the item live in cache 60 minutes */ private static final CacheAccess<String, String> TOKEN_CACHE; static { try { TOKEN_CACHE = JCS.getInstance("default"); } catch (CacheException e) { throw new RuntimeException("Cannot init token cache !", e); } } /** * Add token into the blacklist * * @param token Token for which the hash must be added * @throws NoSuchAlgorithmException If SHA256 is not available */ public static void addToken(String token) throws NoSuchAlgorithmException { if (token != null && !token.trim().isEmpty()) { String hashHex = computeHash(token); if (TOKEN_CACHE.get(hashHex) == null) { TOKEN_CACHE.putSafe(hashHex, hashHex); } } } /** * Check if a token is present in the blacklist * * @param token Token for which the presence of the hash must be verified * @return TRUE if token is blacklisted * @throws NoSuchAlgorithmException If SHA256 is not available */ public static boolean isBlacklisted(String token) throws NoSuchAlgorithmException { boolean exists = false; if (token != null && !token.trim().isEmpty()) { String hashHex = computeHash(token); exists = (TOKEN_CACHE.get(hashHex) != null); } return exists; } /** * Compute the SHA256 hash of a token * * @param token Token for which the hash must be computed * @return The hash encoded in HEX * @throws NoSuchAlgorithmException If SHA256 is not available */ private static String computeHash(String token) throws NoSuchAlgorithmException { String hashHex = null; if (token != null && !token.trim().isEmpty()) { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(token.getBytes()); hashHex = DatatypeConverter.printHexBinary(hash); } return hashHex; } } Message handling - Process a request from a user to add a message in the list. Show a authorization validation approach example import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import org.owasp.pocwebsocket.enumeration.AccessLevel; import org.owasp.pocwebsocket.util.AccessTokenBlacklistUtils; import org.owasp.pocwebsocket.util.AuthenticationUtils; import org.owasp.pocwebsocket.util.MessageUtils; import org.owasp.pocwebsocket.vo.MessageRequest; import org.owasp.pocwebsocket.vo.MessageResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.websocket.EncodeException; import javax.websocket.RemoteEndpoint; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Handle message flow */ public class MessageHandler implements javax.websocket.MessageHandler.Whole<MessageRequest> { private static final Logger LOG = LoggerFactory.getLogger(MessageHandler.class); /** * Reference to the communication channel with the client */ private RemoteEndpoint.Basic clientConnection; /** * Constructor * * @param clientConnection Reference to the communication channel with the client */ public MessageHandler(RemoteEndpoint.Basic clientConnection) { this.clientConnection = clientConnection; } /** * {@inheritDoc} */ @Override public void onMessage(MessageRequest message) { MessageResponse response = null; try { /*Step 1: Verify the token*/ String token = message.getToken(); //Verify if is it in the blacklist if (AccessTokenBlacklistUtils.isBlacklisted(token)) { throw new IllegalAccessException("Token is in the blacklist !"); } //Verify the signature of the token DecodedJWT decodedToken = AuthenticationUtils.validateToken(token); /*Step 2: Verify the authorization (access level)*/ Claim accessLevel = decodedToken.getClaim("access_level"); if (accessLevel == null || AccessLevel.valueOf(accessLevel.asString()) == null) { throw new IllegalAccessException("Token have an invalid access level claim !"); } /*Step 3: Do the expected processing*/ //Init the list of the messages for the current user if (!MessageUtils.MESSAGES_DB.containsKey(decodedToken.getSubject())) { MessageUtils.MESSAGES_DB.put(decodedToken.getSubject(), new ArrayList<>()); } //Add message to the list of message of the user if the message is a not a token invalidation order otherwise add the token to the blacklist if (AccessTokenBlacklistUtils.MESSAGE_ACCESS_TOKEN_INVALIDATION_FLAG.equalsIgnoreCase(message.getContent().trim())) { AccessTokenBlacklistUtils.addToken(message.getToken()); } else { MessageUtils.MESSAGES_DB.get(decodedToken.getSubject()).add(message.getContent()); } //According to the access level of user either return only is message or return all message List<String> messages = new ArrayList<>(); if (accessLevel.asString().equals(AccessLevel.USER.name())) { MessageUtils.MESSAGES_DB.get(decodedToken.getSubject()).forEach(s -> messages.add(String.format("(%s): %s", decodedToken.getSubject(), s))); } else if (accessLevel.asString().equals(AccessLevel.ADMIN.name())) { MessageUtils.MESSAGES_DB.forEach((k, v) -> v.forEach(s -> messages.add(String.format("(%s): %s", k, s)))); } //Build the response object indicating that exchange succeed if (AccessTokenBlacklistUtils.MESSAGE_ACCESS_TOKEN_INVALIDATION_FLAG.equalsIgnoreCase(message.getContent().trim())) { response = new MessageResponse(true, messages, "Token added to the blacklist"); }else{ response = new MessageResponse(true, messages, ""); } } catch (Exception e) { LOG.error("[MessageHandler] Error occur in exchange process.", e); //Build the response object indicating that exchange fail //We send the error detail on client because ware are in POC (it will not the case in a real app) response = new MessageResponse(false, new ArrayList<>(), "Error occur during exchange: " + e.getMessage()); } finally { //Send response try { this.clientConnection.sendObject(response); } catch (IOException | EncodeException e) { LOG.error("[MessageHandler] Error occur in response object sending.", e); } } } } Confidentiality and IntegrityIf the raw version of the protocol is used (protocol WS://) then the transfered data are exposed to eavesdropping and potential on-the-fly alteration. Example of capture using Wireshark and searching for password exchanges in the stored PCAP file, not printable characters has been explicitly removed from the command result: $ grep -aE '(password)' capture.pcap {"login":"bob","password":"bob123"} There is a way to check, at WebSocket endpoint level, if the channel is secure by calling the method isSecure() on the session object instance. Example of implementation in the method of the endpoint in charge of setup the session and affect the message handler: /** * Handle the beginning of an exchange * * @param session Exchange session information */ @OnOpen public void start(Session session) { ... //Affect a new message handler instance in order to process the exchange only if the channel is secured if(session.isSecure()) { session.addMessageHandler(new AuthenticationMessageHandler(session.getBasicRemote())); }else{ LOG.info("[AuthenticationEndpoint] Session {} do not use a secure channel so no message handler was affected for processing and session was explicitly closed !", session.getId()); try{ session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT,"Insecure channel used !")); }catch(IOException e){ LOG.error("[AuthenticationEndpoint] Session {} cannot be explicitly closed !", session.getId(), e); } } LOG.info("[AuthenticationEndpoint] Session {} message handler affected for processing", session.getId()); } Expose WebSocket endpoints only on WSS:// protocol (WebSockets over SSL/TLS) in order to ensure Confidentiality and Integrity of the traffic like using HTTP over SSL/TLS to secure HTTP exchanges. Authors and Primary Editors
Other Cheatsheets |