theme | layout | highlighter | lineNumbers |
---|---|---|---|
slidev-theme-nearform |
default |
shiki |
false |
-
The Open Web Application Security Project (OWASP) is a non profit foundation that works to improve the security of software
-
OWASP has community-led open-source software projects, hundreds of local chapters worldwide, tens of thousands of members, and leading educational and training conferences
- The OWASP Top Ten is a standard awareness document for developers and web application security
- It represents a broad consensus about the most critical security risks to web applications
- This workshop will explain each of the 10 vulnerabilities
- There is a Fastify node.js server demonstrating the security issues
- At each step you are asked to fix the vulnerability in the server
- You will find the solution to each step in the file
solution.js
insidesrc/a{n}-{name}
folder (the actual path may vary) - The 💡 icon indicates hints
- Node LTS
- docker
- docker-compose
- Postman (if you want to be able to test vulnerabilities)
git clone https://github.com/nearform/owasp-top-ten-workshop
npm ci
npm run db:up
npm run db:migrate
cd src/a{x}-step-name
npm start
will run the server ready to respond to requestsnpm run verify
will run automated tests that fail by default until this step's issue is solved- Check out README.md in the projects for potential additional info
cd src/a01-access-control
npm start
The server for that step will run on http://localhost:3000
- Some vulnerabilities involve sending specific requests to the server. We will be using the tool Postman to send those requests
- The
postman
folder contains a collection that can be imported into Postman to easily send those requests - The Postman collection is pre-logged in with user
alice
. TheBearer token
is set that representsalice
.
- Some vulnerabilities have automated tests which verify the presence of the vulnerability
- Those tests will fail until you fix the vulnerability
npm run verify
in a step's folder
- AO1:2021 - Broken Access Control
- User should only act based on specific permissions
- Incorrect permissions -> unauthorised access of data
- Not applying principle of least privilege
- Checks can be bypassed by tampering with page or request
- Allowing access to someone else's info by knowing the UUID
- CORS allows untrusted origins
- Deny access by default
- Avoid duplication of access control logic
- Enforce user ownership when manipulating data
- The
/profile
route returns sensitive user data - It takes a
username
query parameter to return the user's info
GET http://localhost:3000/profile?username=alice
{
"id": 1,
"username": "alice",
"age": 23
}
- Run the server for step 1 (
cd src/a01-access-control
,npm start
) - In Postman, run the query for A01: Access Control. Observe the data for Alice being returned
- Now change the
username
query parameter tobob
. Result:
{
"id": 2,
"username": "bob",
"age": 31
}
- Bob's data is being exposed! Remember we're logged in as Alice and shouldn't see Bob's data
- Run the automated tests for step 1 -
npm run verify
- The tests fail because the server shouldn't return Bob's data
- Edit the
/profile
route in the exercise folder to return only thelogged-in
user's profile without exposing other people's profiles - 💡 The server uses fastify-jwt to handle authentication
- The issue comes from the usage of a user-supplied
query
parameter to choose which profile's info to check - The server should instead fetch the only the
logged-in
user's info and reply with403
in other cases
async req => {
if (!req.user) {
throw new errors.Unauthorized()
}
const username = req.user.username // 💡 We get the username from the logged in user, not from the query!
if (username !== req.query.username) {
throw new errors.Forbidden() // if does not match with the user's one, return a 403 Forbidden error
}
return user
}
- A02: Cryptographic Failures
- Weak or inexistent cryptography of sensitive data
- Passwords, credit card numbers, health records, personal information, business secrets...
- Anything protected by privacy laws or other regulations
- Weak or outdated cryptographic algorithms like md5
⚠️ - Weak secret keys, default ones, keys from online tutorials, keys checked in source control...
- Lack of traffic encryption (HTTPS)
- Insufficient entropy in seed generation
- Check sensitive data is well encrypted. Avoid storing sensitive data unnecessarily
- Use up to date and strong standard algorithms
- Proper key/secrets management (private keys in git
⚠️ ) - Disable caching for responses that contain sensitive data
- The
/all-data
endpoint returns all users and their passwords (hashed using md5). Imagine this as a data breach - Result of the
all-data
endpoint
[
{
"username": "bob",
"password": "884a22eb30e5cfd71894d43ac553faa5"
},
{
"username": "alice",
"password": "5e9d11a14ad1c8dd77e98ef9b53fd1ba"
}
]
- Using the
/all-data
route, find Alice's hashed password - With this password hash, try to find the original password Alice created
- the Postman collection contains requests for doing the queries
- 💡 There are websites to decrypt md5
- md5 hash is vulnerable and shouldn't be used
- In
src/a02-cryptographic-failure
, fix the hashing algorithm used to be a strong algorithm instead of md5 - Using bcrypt is a good idea for passwords
- The application exposes a
/change-password
route used to change a user's password
// utils/crypto.js
import { hash, compare } from 'bcrypt'
const saltRounds = 10
export async function hashPassword(password) {
return await hash(password, saltRounds)
}
// utils/crypto.js
export async function comparePassword(password, hash) {
return await compare(password, hash)
}
- A03: Injection
- Injections are a form of attack where a malicious payload is able to effectively inject an arbitrary bit of query or code on the target server
- Injections can result in data loss or corruption, lack of accountability, or denial of access. Injections can sometimes lead to complete host takeover.
- Common targets: SQL, NoSQL, ORM, LDAP, JS eval
- Lack of validation or sanitization of user input
- Dynamic queries or non-parameterized calls without context-aware escaping are used directly in the interpreter
- Hostile data is used within object-relational mapping (ORM) search parameters to extract additional, sensitive records
- Run the server for step 3 (
cd src/a03-injection
,npm start
) - In Postman, run the query for
A03: Get customer by name
. Observe the data forname: "alice"
being returned - Try to run the query for
A03: SQL Injection
. Observe all the customers being returned - The query param value
' OR '1'='1
takes advantage of the unsafe string concatenation to create this SQL querySELECT * FROM customers WHERE name='' OR '1'='1'
which will return every record in the table
- 💡 Prefer using a safe API that sanitizes input e.g
@nearform/sql
- Escape special characters using the specific escape syntax for that interpreter
- Avoid user-supplied table names or column names as they cannot be escaped
- Automated testing of all parameters, headers, URL, cookies, JSON, SOAP, and XML data inputs is strongly encouraged
- The
@nearform/sql
library escapes special characters contained in the user's input
// import SQL from '@nearform/sql'
export default async function customer(fastify) {
fastify.get(
'/customer',
{
onRequest: [fastify.authenticate]
},
async req => {
const { name } = req.query
const { rows: customers } = await fastify.pg.query(
SQL`SELECT * FROM customers WHERE name=${name}`
)
if (!customers.length) throw errors.NotFound()
return customers
}
)
}
- A04: Insecure Design
- Fundamental design flaws of the software can cause security issues
- Those issues cannot be fixed by a better more secure code implementation
- Failure to determine the level of security required during design
- A forgotten password flow with "security questions" is insecure by design because more than one person can know the answer
- An ecommerce website sells high-end video cards that scalpers buy with bots to resell (bad PR with customers)
- A cinema chain allows booking up to fifteen attendees before requiring a deposit. An attacker could make hundreds of small booking requests at once to block all seats, causing massive revenue loss
- Model threats for the application, all its flows and business logic
- Continuously evaluate security requirement and design during the development lifecycle
- Consider security rules and access controls for every user story
- Use unit and integration tests to verify the application is resistant to the threat model
- The
/buy-product
endpoint does not have protection against bots run by scalpers - It means that someone can buy a lot of stock quickly and leave legitimate customers without any
- Run the server for step 4 (
cd src/a04-insecure-design
,npm start
) - In Postman, run the query for
A04: Buy product
. Observe the data forsuccess: true
being returned - Run the query many times in a row in a short period of time
- Notice that there is no protection against multiple sequential purchases
- Prefer using rate limiter for your routes
- 💡 Using
fastify-rate-limit
is a good idea for setting a rate limit - Let's consider a scenario where a user can buy a maximum of two products per minute
- Edit the
/buy-product
route in the exercise folder considering the scenario above
// register the rateLimit plugin in the server.js file
await fastify.register(rateLimit)
// the rate limit is set to a maximum of two purchases per minute
export default async function ecommerce(fastify) {
fastify.post(
'/buy-product',
{
config: {
rateLimit: {
max: 2,
timeWindow: '1 minute'
}
}
},
(req, reply) => {
reply.send({ success: true })
}
)
}
- Security misconfigurations are security controls that are inaccurately configured or left insecure, putting your systems and data at risk
- Badly configured servers or services can lead to vulnerabilities
- With increased usage of highly configurable software and cloud APIs, there are many opportunities for misconfiguration
- Improperly configured permissions or security settings
- Unnecessary features enabled: open ports, services, accounts with elevated accesss...
- Default credentials unchanged
- Out of date or vulnerable server
- Stack trace from error handling revealing information to users
- Repeatable, automated and fast to deploy environments
- Different credentials should be used in each environment
- Frequently review security updates, patches and permissions
- A segmented architecture increases security by separating components, tenants, containers or cloud security groups
- An automated test to verify the effectiveness of the configurations
- Run the server for step 5 (
cd src/a05-security-misconfiguration
,npm start
) - In Postman, run the query for
A05: Login
. Observe a cookie withuserId=1
being returned - Try to run the query for
A05: Profile
. Observe the information about profile withuserId=1
being returned - Try to change the value of the cookie to
userId=2
. Observe information aboutuserId=2
being returned
- 💡 Cookie must always be signed to ensure they are not getting tampered with on client-side by an attacker
- It's important to use httpOnly cookies to prevent the cookie being accessed through client side script
- Store the signing secret safely.
- Don't store sentive information in cleartext
export function login(fastify) {
fastify.post('/login', { schema }, async (req, rep) => {
const { username, password } = req.body
const {
rows: [user]
} = await fastify.pg.query(
SQL`SELECT id, username, password FROM users WHERE username = ${username}`
)
if (!user) {
throw errors.Unauthorized('No matching user found')
}
const passwordMatch = await comparePassword(password, user.password)
if (!passwordMatch) {
throw errors.Unauthorized('Invalid Password')
}
rep.setCookie('userId', JSON.stringify(user.id), {
signed: true, // 💡 signing the cookie
httpOnly: true // http only
})
return 'user logged in'
})
}
export function profile(fastify) {
fastify.get('/profile', async req => {
const { value: id, valid } = fastify.unsignCookie(
//unsign the cookie and check validity
req?.cookies?.userId || ''
)
if (!valid) {
// check if the cookie has been tampered
throw new errors.Unauthorized()
}
const {
rows: [user]
} = await fastify.pg.query(
SQL`SELECT id, username, age FROM users WHERE id = ${id}`
)
if (!user) {
throw new errors.NotFound()
}
return user
})
}
- Applications use a variety of components and libraries which can have security issues
- Vulnerable components can be an attack vector until they are patched
- Particularly relevant in the node.js world with an ever-growing NPM dependency tree
- Not tracking versions of used components, including nested dependencies
- Using unsupported or vulnerable software, including OS, web server, database, APIs, libraries, components, runtimes...
- Not scanning for vulnerabilities regularly and subscribing to security news for used components
- Not fixing vulnerable dependencies in a timely fashion
- Remove unused dependencies, unnecessary features, components, files, and documentation
- Continuously inventory the versions of components and their dependencies
- Monitor for libraries and components that are unmaintained or do not create security patches for older versions
- Only obtain components from official sources over secure links
- Run the server for step 6 (
cd src/a06-vulnerable-outdated
,npm start
) - In Postman, run the query for
A06: Profile
. Observe error404
being returned - Try to run the query for
A06: Exploit vulnerability
. Observe the error message response
- Because of an outdated version of the HTTP client library undici we can exploit a known vulnerability
- By passing the value
//127.0.0.1
in the username query param, we override the original hostname and we can make the server perform a GET to127.0.0.1:80
- 💡 Update the library to a version in which this vulnerability was fixed
import { request } from 'undici' //💡 updated undici version >= 5.8.1
export default function (fastify) {
fastify.get(
'/profile',
{
onRequest: [fastify.authenticate]
},
async req => {
const { username } = req.query
if (/^\//.test(username)) {
// check username doesn't start with /
throw errors.BadRequest()
}
const { body, statusCode } = await request({
origin: 'http://example.com',
pathname: username
})
if (statusCode !== 200) {
throw errors.NotFound()
}
return body
}
)
}
- Verification of the user's identity, authentication, and session management is crucial to security
- Weak or vulnerable authentication systems can be exploited to gain access
- Systems with broken authentication can lead to data breaches and passwords leak
- Application allows for credentials stuffing or brute forcing
- Allows default, weak or known passwords
- Exploitable credential recovery processes
- Lack of effective multi-factor authentication
- Unencrypted or weakly encrypted password storage
- Where possible, implement multi-factor authentication
- Require strong passwords (length, complexity, rotation policies, and don't allow leaked passwords use)
- Ensure registration and credential recovery use the same messages for all outcomes
- Limit or increasingly delay failed login attempts
- Use secure password data store practices (salting + hashing)
- In 2017 the NIST recommended that websites should check all new passwords against available lists of data breaches.
- This practice has been adopted by OWASP and became part of their recommendation.
- In the real scenario you should try to use something like Have I Been Pwned
- In the workshop - the database contains the list of leaked passwords in
databreachrecords
- Run the server for step 7 (
cd src/a07-authentication-failures
,npm start
) - In Postman, run the query for
A07: Register
. Observe a token is succesfully returned - In the database check the
databreachrecords
table for password used in the Postman request body
- It should not allow to use passwords that are known to be leaked
- Instead it should require the user to set a different password indicating what is the reason
- Place your solution in the
routes/user/index.js
- Test it by running
npm run verify
(it will fail initially)
- 💡 Using data available in
dataBreachRecords
check if requested password is safe to use - Run
sql
query inside the/register
endpoint to check if the password is there - Return a
400
error withmessage
indicating the source of the leak:'You are trying to use password that is known to be exposed in data breaches: ${source}. Use a different one. Read more here: https://haveibeenpwned.com/Passwords.'
const {
rows: [breach]
} = await fastify.pg.query(
SQL`SELECT * FROM databreachrecords WHERE password=${password}`
)
if (breach) {
res.send(
errors.BadRequest(
`You are trying to use password that is known to be exposed in data breaches: ${breach.source}. Use a different one. Read more here: https://haveibeenpwned.com/Passwords.`
)
)
}
- Code and infrastructure not protected against integrity violations
- Attackers gaining access to a plugin and deploy an unverified update which would get distributed to all its users
- Libraries coming from untrusted sources, repos or CDNs
- Deserialization of Untrusted Data, where objects or data are encoded or serialized into a structure that an attacker can see and modify is vulnerable to insecure deserialization
- An insecure CI/CD pipeline
- Updates downloaded without sufficient integrity verification
- Using digital signatures for integrity checks on data and downloaded software
- Ensure npm dependencies are trusted. For higher risks, host a custom repository of packages with internally vetted dependencies
- Use automated tools to verify that components don't contain known vulnerabilities
- Ensure there are code reviews for changes to minimise the risk of malicious code being introduced
- Run the server for step 8 (
cd src/a08-software-data-integrity-failures
,npm start
) - In Postman, try to run the query for
A08: Get profile from cookie
. There is a cookie attached to the request/profile
containing the user's profile encoded as base64. Observe the requeststatus code 500
being returned - This is happening because the server is deserializing a
cookie containing a malicious JavaScript
code which is forcing the server to throw an exception
- When decoding the cookie attached to the
/profile
request, this is what the output looks like:
// base64 to ASCII
{
id: 1,
username:
"_$$ND_FUNC$$_function () {\n throw new Error('server error')\n }()"
}
- Note that there is an
IIFE
at the end of the username key, and this causes the function to run on the server as it is doing an insecure deserialization - The full step by step to serialize a JavaScript code and inject it as a cookie can be found in this article
- Run the automated tests for step 8 -
npm run verify
- The tests fail because the server shouldn't trust a library that provides a way to deserialize strings into executable JavaScript code
- Untrusted data passed into
unserialize()
function innode-serialize
module can be exploited to achieve arbitrary code execution by passing a serialized JavaScript Object with an Immediately invoked function expression (IIFE) - 💡
JSON.parse
is a safer way to deserialize data
export default async function solution(fastify) {
fastify.get('/profile', req => {
const cookieAsStr = Buffer.from(req.cookies.profile, 'base64').toString(
'ascii'
)
const profile = JSON.parse(cookieAsStr)
if (profile.username) {
return 'Hello ' + profile.username
}
return 'Hello guest'
})
}
- Proper logging and monitoring is critical to detecting and responding to breaches
- It is important for alerting, accountability, visibility and forensics of security incidents
- Auditable events, such as logins, failed logins, and high-value transactions, are not logged
- Warnings and errors generate no, inadequate, or unclear log messages
- Logs of applications and APIs are not monitored for suspicious activity
- The application cannot detect, escalate, or alert for active attacks in real-time or near real-time
- Ensure all login, access control, and server-side input validation failures can be logged with sufficient user context to identify suspicious or malicious accounts
- Ensure log data is encoded correctly to prevent injections or attacks on the logging or monitoring systems
- Ensure high-value transactions have an audit trail with integrity controls to prevent tampering or deletion, such as append-only database tables or similar.
- Run the server for step 5 (
cd src/a09-security-logging
,npm run verify
) - You can observe a log message
something suspicious is happening
- Note that the application is using a vulnerable version of the http client undici
- 💡 Log user input to identify suspicious or malicious access
- Validate user input
// profile route handler
async req => {
console.log({
username: req.user.username, // add context to logs to help identify the user
input: req.headers['content-type']
})
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ // validate user input
if (headerCharRegex.exec(req.headers['content-type']) !== null) {
throw errors.BadRequest()
}
const { body } = await request('http://localhost:3001', {
method: 'GET',
headers: {
'content-type': req.headers['content-type']
}
})
return body
}
- SSRF flaws occur whenever a web application is fetching a remote resource without validating the user-supplied URL
- It allows an attacker to coerce the application to send a crafted request to an unexpected destination, even when protected by a firewall, VPN, or another type of network access control list
- Sanitize and validate all client-supplied input data
- Enforce the URL schema, port, and destination with a positive allow list
- Do not send raw responses to clients
- Disable HTTP redirections
- ❗ Do not mitigate SSRF via the use of a deny list or regular expression
- Run the server for step 10 (
cd src/a10-server-side-request-forgery
,npm start
) - In Postman, run the query for
A10: Upload Image
. Observe an image being returned - Try to run the query for
A10: Malicious Image url
. Observesomething suspicious is happening
being returned - The server is not sanitizing user input so it will send a request to whatever url provided in the payload
- Sanitize the url making sure it's valid
- Create a whitelist of allowed domains by adding them in the database column
allowedImageDomain
- 💡 Make sure the requested domain is in the whitelist
export default async function profilePicture(fastify) {
fastify.post(
'/user/image',
{
onRequest: [fastify.authenticate]
},
async req => {
const { imgUrl } = req.body
const url = validateUrl(imgUrl) // validate url using the URL object
const {
rows: [whitelisted]
} = await fastify.pg.query(
SQL`SELECT * FROM allowedImageDomain WHERE hostname = ${url.hostname}`
)
if (!whitelisted) {
throw errors.Forbidden()
}
const { data } = await axios.get(url.href)
return data
}
)
}
- The OWASP Top 10 is a good introduction to the most common issues to keep in mind while developing
- By its nature, the Top 10 is a series of guidelines but can't be tested easily as its applications are broad
- On the other hand the OWASP Application Security Verification Standard is a comprehensive list of security criteria
- Those are specific, standardised and verifiable security requirements on which on app can be tested to either pass or fail
- The OWASP Cheat Sheet Series provides condensed information on specific security topics related to OWASP standards
- The OWASP Software Assurance Maturity Model can help an organization analyze and improve their software security