├── index.js ├── .nvmrc ├── .npmrc ├── .husky ├── pre-commit └── commit-msg ├── migrations ├── 001.undo.drop-table.sql ├── 002.undo.delete-users.sql ├── 003.undo.delete-customers.sql ├── 005.undo.drop-table.sql ├── 004.undo.delete-data-breach-records.sql ├── 001.do.create-table.sql ├── 005.do.add-domain.sql ├── 002.do.add-users.sql ├── 004.do.add-data-breach-records.sql └── 003.do.add-customers.sql ├── .vscode └── settings.json ├── .commitlintrc ├── .eslintignore ├── assets └── owasp.png ├── .prettierrc ├── index.html ├── src ├── shared │ ├── README.md │ ├── package.json │ ├── .env │ ├── index.js │ ├── test-utils.js │ ├── export-solution.js │ ├── env.js │ ├── plugins │ │ └── authenticate.js │ ├── start-server.js │ ├── routes │ │ ├── login.js │ │ └── register.js │ └── build-server.js ├── a03-injection │ ├── index.js │ ├── README.md │ ├── server.js │ ├── package.json │ ├── routes │ │ └── customer │ │ │ ├── solution.js │ │ │ └── index.js │ └── step3.test.js ├── a01-access-control │ ├── index.js │ ├── server.js │ ├── README.md │ ├── package.json │ ├── routes │ │ └── profile │ │ │ ├── index.js │ │ │ └── solution.js │ └── step1.test.js ├── a04-insecure-design │ ├── index.js │ ├── README.md │ ├── routes │ │ └── ecommerce │ │ │ ├── solution.js │ │ │ └── index.js │ ├── package.json │ ├── server.js │ └── step4.test.js ├── a02-cryptographic-failure │ ├── index.js │ ├── utils │ │ ├── solution.js │ │ └── crypto.js │ ├── server.js │ ├── README.md │ ├── package.json │ ├── routes │ │ ├── passwordsExploit.js │ │ └── changePassword.js │ └── step2.test.js ├── a06-vulnerable-outdated │ ├── index.js │ ├── README.md │ ├── server.js │ ├── package.json │ ├── routes │ │ └── profile │ │ │ ├── solution.js │ │ │ └── index.js │ └── step6.test.js ├── a07-authentication-failures │ ├── index.js │ ├── package.json │ ├── README.md │ ├── server.js │ ├── step7.test.js │ └── routes │ │ └── user │ │ ├── index.js │ │ └── solution.js ├── a05-security-misconfiguration │ ├── index.js │ ├── README.md │ ├── package.json │ ├── server.js │ ├── routes │ │ └── user │ │ │ ├── solution.js │ │ │ └── index.js │ └── step5.test.js ├── a08-software-data-integrity-failures │ ├── index.js │ ├── serializer.js │ ├── server.js │ ├── README.md │ ├── package.json │ ├── routes │ │ └── profile │ │ │ ├── solution.js │ │ │ └── index.js │ └── step8.test.js ├── a09-security-logging │ ├── index.js │ ├── README.md │ ├── package.json │ ├── server.js │ ├── routes │ │ └── profile │ │ │ ├── index.js │ │ │ └── solution.js │ └── step9.test.js └── a10-server-side-request-forgery │ ├── index.js │ ├── README.md │ ├── server.js │ ├── package.json │ ├── routes │ └── user │ │ ├── index.js │ │ └── solution.js │ └── step10.test.js ├── .editorconfig ├── components └── Copyright.ts ├── docker-compose.yml ├── .postgratorrc.json ├── .gitignore ├── .eslintrc ├── .github ├── dependabot.yml └── workflows │ ├── notify-release.yml │ ├── deploy.yml │ ├── check-linked-issues.yml │ ├── release.yml │ └── ci.yml ├── styles.css ├── README.md ├── package.json ├── public └── images │ └── nearform.svg ├── components.d.ts ├── postman └── OWASP Top Ten Workshop.postman_collection.json ├── LICENSE └── slides.md /index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | if-present = true 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit $1 2 | -------------------------------------------------------------------------------- /migrations/001.undo.drop-table.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE users; 2 | -------------------------------------------------------------------------------- /migrations/002.undo.delete-users.sql: -------------------------------------------------------------------------------- 1 | TRUNCATE TABLE users; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.insertFinalNewline": true 3 | } -------------------------------------------------------------------------------- /migrations/003.undo.delete-customers.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE customers; 2 | -------------------------------------------------------------------------------- /migrations/005.undo.drop-table.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE allowedImageDomain; 2 | -------------------------------------------------------------------------------- /migrations/004.undo.delete-data-breach-records.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE dataBreachRecords; 2 | -------------------------------------------------------------------------------- /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | dist/ 4 | public/ 5 | migrations/ 6 | theme/ 7 | -------------------------------------------------------------------------------- /assets/owasp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/owasp-top-ten-workshop/HEAD/assets/owasp.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "arrowParens": "avoid", 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/README.md: -------------------------------------------------------------------------------- 1 | # Shared code 2 | 3 | This folder contains shared utility/setup code used across the different examples 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 -------------------------------------------------------------------------------- /components/Copyright.ts: -------------------------------------------------------------------------------- 1 | const Copyright = () => `© Copyright ${new Date().getFullYear()} Nearform Ltd. All Rights Reserved.` 2 | 3 | export default Copyright 4 | -------------------------------------------------------------------------------- /src/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owasp-shared", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "main": "index.js" 7 | } 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | services: 3 | postgres: 4 | image: postgres:alpine 5 | environment: 6 | POSTGRES_PASSWORD: owasp10 7 | ports: 8 | - '5434:5432' 9 | -------------------------------------------------------------------------------- /src/shared/.env: -------------------------------------------------------------------------------- 1 | PG_CONNECTION_STRING=postgres://postgres:owasp10@localhost:5434/postgres 2 | JWT_SECRET=supersecret 3 | LOG_LEVEL=error 4 | PRETTY_PRINT=true 5 | PRINT_ROUTES=true 6 | USE_SOLUTION=false 7 | COOKIES_SECRET= secret 8 | -------------------------------------------------------------------------------- /.postgratorrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrationPattern": "migrations/*", 3 | "driver": "pg", 4 | "host": "127.0.0.1", 5 | "port": 5434, 6 | "database": "postgres", 7 | "username": "postgres", 8 | "password": "owasp10" 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build 2 | dist/ 3 | 4 | # tests 5 | coverage/ 6 | .nyc_output/ 7 | 8 | # node 9 | node_modules 10 | .eslintcache 11 | 12 | # JetBrains IDEs 13 | .idea 14 | 15 | # OS X 16 | .DS_Store 17 | 18 | # tap reports 19 | .tap 20 | -------------------------------------------------------------------------------- /migrations/001.do.create-table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users( 2 | id SERIAL PRIMARY KEY, 3 | username VARCHAR (50) NOT NULL, 4 | password VARCHAR(100) NOT NULL, 5 | age SMALLINT NOT NULL, 6 | credit_card_number VARCHAR(16) NOT NULL 7 | ) 8 | -------------------------------------------------------------------------------- /migrations/005.do.add-domain.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE TABLE allowedImageDomain( 3 | id SERIAL PRIMARY KEY, 4 | hostname VARCHAR (50) NOT NULL 5 | ); 6 | 7 | INSERT INTO allowedImageDomain ( 8 | hostname 9 | ) VALUES ( 10 | 'i.imgflip.com' 11 | ); 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 3 | "env": { 4 | "node": true, 5 | "es2021": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2021, 9 | "sourceType": "module" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /src/a03-injection/index.js: -------------------------------------------------------------------------------- 1 | import { step3Server } from './server.js' 2 | import { startServer } from 'owasp-shared' 3 | 4 | const start = async function () { 5 | const fastify = await step3Server() 6 | startServer(fastify) 7 | } 8 | 9 | start() 10 | -------------------------------------------------------------------------------- /src/a01-access-control/index.js: -------------------------------------------------------------------------------- 1 | import { step1Server } from './server.js' 2 | import { startServer } from 'owasp-shared' 3 | 4 | const start = async function () { 5 | const fastify = await step1Server() 6 | startServer(fastify) 7 | } 8 | 9 | start() 10 | -------------------------------------------------------------------------------- /src/a04-insecure-design/index.js: -------------------------------------------------------------------------------- 1 | import { step4Server } from './server.js' 2 | import { startServer } from 'owasp-shared' 3 | 4 | const start = async function () { 5 | const fastify = await step4Server() 6 | startServer(fastify) 7 | } 8 | 9 | start() 10 | -------------------------------------------------------------------------------- /src/a02-cryptographic-failure/index.js: -------------------------------------------------------------------------------- 1 | import { step2Server } from './server.js' 2 | import { startServer } from 'owasp-shared' 3 | 4 | const start = async function () { 5 | const fastify = await step2Server() 6 | startServer(fastify) 7 | } 8 | 9 | start() 10 | -------------------------------------------------------------------------------- /src/a06-vulnerable-outdated/index.js: -------------------------------------------------------------------------------- 1 | import { step6Server } from './server.js' 2 | import { startServer } from 'owasp-shared' 3 | 4 | const start = async function () { 5 | const fastify = await step6Server() 6 | startServer(fastify) 7 | } 8 | 9 | start() 10 | -------------------------------------------------------------------------------- /src/a07-authentication-failures/index.js: -------------------------------------------------------------------------------- 1 | import { startServer } from 'owasp-shared' 2 | import { step7Server } from './server.js' 3 | 4 | const start = async function () { 5 | const fastify = await step7Server() 6 | startServer(fastify) 7 | } 8 | 9 | start() 10 | -------------------------------------------------------------------------------- /src/a05-security-misconfiguration/index.js: -------------------------------------------------------------------------------- 1 | import { startServer } from 'owasp-shared' 2 | import { step5Server } from './server.js' 3 | 4 | const start = async function () { 5 | const fastify = await step5Server() 6 | startServer(fastify) 7 | } 8 | 9 | start() 10 | -------------------------------------------------------------------------------- /src/a08-software-data-integrity-failures/index.js: -------------------------------------------------------------------------------- 1 | import { step8Server } from './server.js' 2 | import { startServer } from 'owasp-shared' 3 | 4 | const start = async function () { 5 | const fastify = await step8Server() 6 | startServer(fastify) 7 | } 8 | 9 | start() 10 | -------------------------------------------------------------------------------- /src/a08-software-data-integrity-failures/serializer.js: -------------------------------------------------------------------------------- 1 | import serialize from 'node-serialize' 2 | 3 | const fakeCookie = { 4 | id: 1, 5 | username: function () { 6 | throw new Error('server error') 7 | } 8 | } 9 | 10 | console.log(serialize.serialize(fakeCookie)) 11 | -------------------------------------------------------------------------------- /src/shared/index.js: -------------------------------------------------------------------------------- 1 | import { buildServer } from './build-server.js' 2 | import { env } from './env.js' 3 | import { startServer, startTargetServer } from './start-server.js' 4 | import { authHeaders } from './test-utils.js' 5 | 6 | export { buildServer, env, startServer, authHeaders, startTargetServer } 7 | -------------------------------------------------------------------------------- /src/a09-security-logging/index.js: -------------------------------------------------------------------------------- 1 | import { startServer, startTargetServer } from 'owasp-shared' 2 | import { step9Server } from './server.js' 3 | 4 | const start = async function () { 5 | const fastify = await step9Server() 6 | startServer(fastify) 7 | } 8 | 9 | start() 10 | startTargetServer(() => {}) 11 | -------------------------------------------------------------------------------- /src/a10-server-side-request-forgery/index.js: -------------------------------------------------------------------------------- 1 | import { step10Server } from './server.js' 2 | import { startServer, startTargetServer } from 'owasp-shared' 3 | 4 | const start = async function () { 5 | const fastify = await step10Server() 6 | startServer(fastify) 7 | } 8 | 9 | start() 10 | startTargetServer(() => {}) 11 | -------------------------------------------------------------------------------- /src/shared/test-utils.js: -------------------------------------------------------------------------------- 1 | // Helper to easily use a premade logged in token 2 | 3 | export const loggedInToken = 4 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhbGljZSIsImlhdCI6MTY2MjYzNzc2MH0.15w1NA_Kol5146DJEdXbDuIMmbVsiBXSGgzsVrV5NTY' 5 | export const authHeaders = { 6 | authorization: 'Bearer ' + loggedInToken 7 | } 8 | -------------------------------------------------------------------------------- /src/a02-cryptographic-failure/utils/solution.js: -------------------------------------------------------------------------------- 1 | import { hash, compare } from 'bcrypt' 2 | 3 | const saltRounds = 10 4 | 5 | export async function hashPassword(password) { 6 | return await hash(password, saltRounds) 7 | } 8 | 9 | export async function comparePassword(password, hash) { 10 | return await compare(password, hash) 11 | } 12 | -------------------------------------------------------------------------------- /migrations/002.do.add-users.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO users ( 2 | username, 3 | password, 4 | age, 5 | credit_card_number 6 | ) VALUES ( 7 | 'alice', 8 | '482c811da5d5b4bc6d497ffa98491e38', 9 | 23, 10 | '1234567890123456' 11 | ), ( 12 | 'bob', 13 | '884a22eb30e5cfd71894d43ac553faa5', 14 | 31, 15 | '1234567890123456' 16 | ); 17 | -------------------------------------------------------------------------------- /src/a02-cryptographic-failure/server.js: -------------------------------------------------------------------------------- 1 | import { buildServer } from 'owasp-shared' 2 | import { env } from 'owasp-shared' 3 | 4 | export async function step2Server() { 5 | const fastify = await buildServer({ 6 | baseDir: import.meta.url, 7 | env, 8 | fastifyOptions: {}, 9 | autoloadRoutes: true 10 | }) 11 | return fastify 12 | } 13 | -------------------------------------------------------------------------------- /src/a03-injection/README.md: -------------------------------------------------------------------------------- 1 | # Exercise 3: Injection 2 | 3 | See slides for info about the exercise. 4 | 5 | Run the automated tests for verifying the exercise: 6 | 7 | `npm run verify` 8 | 9 | Start the server for manual tests: 10 | 11 | `npm start` 12 | 13 | Use the Postman collection at the root of the repo for ready to use queries matching this problem 14 | -------------------------------------------------------------------------------- /src/a01-access-control/server.js: -------------------------------------------------------------------------------- 1 | import { buildServer, env } from 'owasp-shared' 2 | import profileRoute from './routes/profile/index.js' 3 | 4 | export async function step1Server() { 5 | const fastify = await buildServer({ 6 | baseDir: import.meta.url, 7 | env, 8 | fastifyOptions: {} 9 | }) 10 | profileRoute(fastify) 11 | return fastify 12 | } 13 | -------------------------------------------------------------------------------- /src/a03-injection/server.js: -------------------------------------------------------------------------------- 1 | import { buildServer, env } from 'owasp-shared' 2 | import customerRoute from './routes/customer/index.js' 3 | 4 | export async function step3Server() { 5 | const fastify = await buildServer({ 6 | baseDir: import.meta.url, 7 | env, 8 | fastifyOptions: {} 9 | }) 10 | customerRoute(fastify) 11 | return fastify 12 | } 13 | -------------------------------------------------------------------------------- /src/a01-access-control/README.md: -------------------------------------------------------------------------------- 1 | # Exercise 1: Access Control 2 | 3 | See slides for info about the exercise. 4 | 5 | Run the automated tests for verifying the exercise: 6 | 7 | `npm run verify` 8 | 9 | Start the server for manual tests: 10 | 11 | `npm start` 12 | 13 | Use the Postman collection at the root of the repo for ready to use queries matching this problem 14 | -------------------------------------------------------------------------------- /src/a04-insecure-design/README.md: -------------------------------------------------------------------------------- 1 | # Exercise 4: Insecure Design 2 | 3 | See slides for info about the exercise. 4 | 5 | Run the automated tests for verifying the exercise: 6 | 7 | `npm run verify` 8 | 9 | Start the server for manual tests: 10 | 11 | `npm start` 12 | 13 | Use the Postman collection at the root of the repo for ready to use queries matching this problem 14 | -------------------------------------------------------------------------------- /src/a09-security-logging/README.md: -------------------------------------------------------------------------------- 1 | # Exercise 9: Security logging 2 | 3 | See slides for info about the exercise. 4 | 5 | Run the automated tests for verifying the exercise: 6 | 7 | `npm run verify` 8 | 9 | Start the server for manual tests: 10 | 11 | `npm start` 12 | 13 | Use the Postman collection at the root of the repo for ready to use queries matching this problem 14 | -------------------------------------------------------------------------------- /migrations/004.do.add-data-breach-records.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE dataBreachRecords( 2 | id SERIAL PRIMARY KEY, 3 | password VARCHAR(100) NOT NULL, 4 | source VARCHAR (50) NOT NULL 5 | ); 6 | 7 | INSERT INTO dataBreachRecords ( 8 | password, 9 | source 10 | ) VALUES ( 11 | 'L3Ak_3d-Lik3_N0-t0M0rr0W', 12 | 'adobe' 13 | ), ( 14 | 'IamY0u', 15 | 'linkedin' 16 | ); 17 | 18 | -------------------------------------------------------------------------------- /src/a02-cryptographic-failure/README.md: -------------------------------------------------------------------------------- 1 | # Exercise 2: Cryptographic Failure 2 | 3 | See slides for info about the exercise. 4 | 5 | Run the automated tests for verifying the exercise: 6 | 7 | `npm run verify` 8 | 9 | Start the server for manual tests: 10 | 11 | `npm start` 12 | 13 | Use the Postman collection at the root of the repo for ready to use queries matching this problem 14 | -------------------------------------------------------------------------------- /src/a08-software-data-integrity-failures/server.js: -------------------------------------------------------------------------------- 1 | import { buildServer, env } from 'owasp-shared' 2 | import profileRoute from './routes/profile/index.js' 3 | 4 | export async function step8Server() { 5 | const fastify = await buildServer({ 6 | baseDir: import.meta.url, 7 | env, 8 | fastifyOptions: {} 9 | }) 10 | profileRoute(fastify) 11 | return fastify 12 | } 13 | -------------------------------------------------------------------------------- /src/a03-injection/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owasp-step-3", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "verify": "node --test", 9 | "test": "cross-env USE_SOLUTION=true node --test", 10 | "start": "nodemon index.js" 11 | }, 12 | "dependencies": { 13 | "owasp-shared": "1.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/a04-insecure-design/routes/ecommerce/solution.js: -------------------------------------------------------------------------------- 1 | export default async function solution(fastify) { 2 | fastify.post( 3 | '/buy-product', 4 | { 5 | config: { 6 | rateLimit: { 7 | max: 2, 8 | timeWindow: '1 minute' 9 | } 10 | } 11 | }, 12 | (req, reply) => { 13 | reply.send({ success: true }) 14 | } 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/a01-access-control/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owasp-step-1", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "verify": "node --test", 9 | "test": "cross-env USE_SOLUTION=true node --test", 10 | "start": "nodemon index.js" 11 | }, 12 | "dependencies": { 13 | "owasp-shared": "1.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/a05-security-misconfiguration/README.md: -------------------------------------------------------------------------------- 1 | # Exercise 5: Security misconfiguration 2 | 3 | See slides for info about the exercise. 4 | 5 | Run the automated tests for verifying the exercise: 6 | 7 | `npm run verify` 8 | 9 | Start the server for manual tests: 10 | 11 | `npm start` 12 | 13 | Use the Postman collection at the root of the repo for ready to use queries matching this problem 14 | -------------------------------------------------------------------------------- /src/a06-vulnerable-outdated/README.md: -------------------------------------------------------------------------------- 1 | # Exercise 6: Vulnerable and Outdated components 2 | 3 | See slides for info about the exercise. 4 | 5 | Run the automated tests for verifying the exercise: 6 | 7 | `npm run verify` 8 | 9 | Start the server for manual tests: 10 | 11 | `npm start` 12 | 13 | Use the Postman collection at the root of the repo for ready to use queries matching this problem 14 | -------------------------------------------------------------------------------- /src/a10-server-side-request-forgery/README.md: -------------------------------------------------------------------------------- 1 | # Exercise 10: Server Side Request Forgery 2 | 3 | See slides for info about the exercise. 4 | 5 | Run the automated tests for verifying the exercise: 6 | 7 | `npm run verify` 8 | 9 | Start the server for manual tests: 10 | 11 | `npm start` 12 | 13 | Use the Postman collection at the root of the repo for ready to use queries matching this problem 14 | -------------------------------------------------------------------------------- /src/a02-cryptographic-failure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owasp-step-2", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "verify": "node --test", 9 | "test": "cross-env USE_SOLUTION=true node --test", 10 | "start": "nodemon index.js" 11 | }, 12 | "dependencies": { 13 | "owasp-shared": "1.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/a07-authentication-failures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owasp-step-7", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "verify": "node --test", 9 | "test": "cross-env USE_SOLUTION=true node --test", 10 | "start": "nodemon index.js" 11 | }, 12 | "dependencies": { 13 | "owasp-shared": "1.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/a07-authentication-failures/README.md: -------------------------------------------------------------------------------- 1 | # Exercise 7: Identification and Authentication Failures 2 | 3 | See slides for info about the exercise. 4 | 5 | Run the automated tests for verifying the exercise: 6 | 7 | `npm run verify` 8 | 9 | Start the server for manual tests: 10 | 11 | `npm start` 12 | 13 | Use the Postman collection at the root of the repo for ready to use queries matching this problem 14 | -------------------------------------------------------------------------------- /src/a08-software-data-integrity-failures/README.md: -------------------------------------------------------------------------------- 1 | # Exercise 8: Software and Data Integrity Failures 2 | 3 | See slides for info about the exercise. 4 | 5 | Run the automated tests for verifying the exercise: 6 | 7 | `npm run verify` 8 | 9 | Start the server for manual tests: 10 | 11 | `npm start` 12 | 13 | Use the Postman collection at the root of the repo for ready to use queries matching this problem 14 | -------------------------------------------------------------------------------- /src/a09-security-logging/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owasp-step-9", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "verify": "node --test", 9 | "test": "cross-env USE_SOLUTION=true node --test", 10 | "start": "nodemon index.js" 11 | }, 12 | "dependencies": { 13 | "undici-5.8.0": "npm:undici@5.8.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/a09-security-logging/server.js: -------------------------------------------------------------------------------- 1 | import { buildServer, env } from 'owasp-shared' 2 | import profileRoute from './routes/profile/index.js' 3 | 4 | export async function step9Server() { 5 | const fastify = await buildServer({ 6 | baseDir: import.meta.url, 7 | env, 8 | fastifyOptions: {}, 9 | excludeSharedRoutes: true 10 | }) 11 | 12 | profileRoute(fastify) 13 | return fastify 14 | } 15 | -------------------------------------------------------------------------------- /src/a10-server-side-request-forgery/server.js: -------------------------------------------------------------------------------- 1 | import { buildServer, env } from 'owasp-shared' 2 | import profilePicture from './routes/user/index.js' 3 | export async function step10Server() { 4 | const fastify = await buildServer({ 5 | baseDir: import.meta.url, 6 | env, 7 | fastifyOptions: {}, 8 | excludeSharedRoutes: true 9 | }) 10 | profilePicture(fastify) 11 | return fastify 12 | } 13 | -------------------------------------------------------------------------------- /src/a07-authentication-failures/server.js: -------------------------------------------------------------------------------- 1 | import { buildServer, env } from 'owasp-shared' 2 | import { registerRoute } from './routes/user/index.js' 3 | 4 | export async function step7Server() { 5 | const fastify = await buildServer({ 6 | baseDir: import.meta.url, 7 | env, 8 | fastifyOptions: {}, 9 | excludeSharedRoutes: true 10 | }) 11 | registerRoute(fastify) 12 | return fastify 13 | } 14 | -------------------------------------------------------------------------------- /src/a06-vulnerable-outdated/server.js: -------------------------------------------------------------------------------- 1 | import { buildServer, env } from 'owasp-shared' 2 | import profileRoute from './routes/profile/index.js' 3 | 4 | export async function step6Server() { 5 | const fastify = await buildServer({ 6 | baseDir: import.meta.url, 7 | env, 8 | fastifyOptions: {}, 9 | excludeSharedRoutes: true 10 | }) 11 | 12 | profileRoute(fastify) 13 | 14 | return fastify 15 | } 16 | -------------------------------------------------------------------------------- /src/a04-insecure-design/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owasp-step-4", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "verify": "node --test", 9 | "test": "cross-env USE_SOLUTION=true node --test", 10 | "start": "nodemon index.js" 11 | }, 12 | "dependencies": { 13 | "@fastify/rate-limit": "^10.3.0", 14 | "owasp-shared": "1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/a04-insecure-design/routes/ecommerce/index.js: -------------------------------------------------------------------------------- 1 | import solution from './solution.js' 2 | import { getSolutionToExport } from 'owasp-shared/export-solution.js' 3 | 4 | async function ecommerce(fastify) { 5 | fastify.post('/buy-product', (req, reply) => { 6 | reply.send({ success: true }) 7 | }) 8 | } 9 | 10 | // Note: This helper just helps with internal unit testing 11 | export default getSolutionToExport(ecommerce, solution) 12 | -------------------------------------------------------------------------------- /src/a10-server-side-request-forgery/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owasp-step-10", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "verify": "node --test", 9 | "test": "cross-env USE_SOLUTION=true node --test", 10 | "start": "nodemon index.js" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.13.2", 14 | "owasp-shared": "1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/a02-cryptographic-failure/routes/passwordsExploit.js: -------------------------------------------------------------------------------- 1 | import SQL from '@nearform/sql' 2 | 3 | // This route exposes all users and their passwords for the purposes of simulating a data breach 4 | export default async function register(fastify) { 5 | fastify.get('/all-data', async () => { 6 | const { rows } = await fastify.pg.query( 7 | SQL`SELECT username, password FROM users` 8 | ) 9 | 10 | return rows 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/a05-security-misconfiguration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owasp-step-5", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "verify": "node --test", 9 | "test": "cross-env USE_SOLUTION=true node --test", 10 | "start": "nodemon index.js" 11 | }, 12 | "dependencies": { 13 | "@fastify/cookie": "^11.0.2", 14 | "owasp-shared": "1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/a06-vulnerable-outdated/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owasp-step-6", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "verify": "node --test", 9 | "test": "cross-env USE_SOLUTION=true node --test", 10 | "start": "nodemon index.js" 11 | }, 12 | "dependencies": { 13 | "owasp-shared": "1.0.0", 14 | "undici-5.8.0": "npm:undici@5.8.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/a05-security-misconfiguration/server.js: -------------------------------------------------------------------------------- 1 | import { buildServer, env } from 'owasp-shared' 2 | import { loginRoute, profileRoute } from './routes/user/index.js' 3 | 4 | export async function step5Server() { 5 | const fastify = await buildServer({ 6 | baseDir: import.meta.url, 7 | env, 8 | fastifyOptions: {}, 9 | excludeSharedRoutes: true 10 | }) 11 | loginRoute(fastify) 12 | profileRoute(fastify) 13 | return fastify 14 | } 15 | -------------------------------------------------------------------------------- /src/a08-software-data-integrity-failures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owasp-step-8", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "verify": "node --test", 9 | "test": "cross-env USE_SOLUTION=true node --test", 10 | "start": "nodemon index.js" 11 | }, 12 | "dependencies": { 13 | "node-serialize": "^0.0.4", 14 | "owasp-shared": "1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/a08-software-data-integrity-failures/routes/profile/solution.js: -------------------------------------------------------------------------------- 1 | export default async function solution(fastify) { 2 | fastify.get('/profile', req => { 3 | const cookieAsStr = Buffer.from(req.cookies.profile, 'base64').toString( 4 | 'ascii' 5 | ) 6 | 7 | const profile = JSON.parse(cookieAsStr) 8 | 9 | if (profile.username) { 10 | return 'Hello ' + profile.username 11 | } 12 | 13 | return 'Hello guest' 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/a04-insecure-design/server.js: -------------------------------------------------------------------------------- 1 | import { buildServer, env } from 'owasp-shared' 2 | import ecommerceRoute from './routes/ecommerce/index.js' 3 | import rateLimit from '@fastify/rate-limit' 4 | 5 | export async function step4Server() { 6 | const fastify = await buildServer({ 7 | baseDir: import.meta.url, 8 | env, 9 | fastifyOptions: {} 10 | }) 11 | 12 | await fastify.register(rateLimit) 13 | 14 | ecommerceRoute(fastify) 15 | return fastify 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/notify-release.yml: -------------------------------------------------------------------------------- 1 | name: Notify release 2 | 'on': 3 | workflow_dispatch: 4 | schedule: 5 | - cron: 30 8 * * * 6 | release: 7 | types: 8 | - published 9 | issues: 10 | types: 11 | - closed 12 | jobs: 13 | setup: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | issues: write 17 | contents: read 18 | steps: 19 | - uses: nearform-actions/github-action-notify-release@v1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /migrations/003.do.add-customers.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE customers( 2 | id SERIAL PRIMARY KEY, 3 | name VARCHAR (20) NOT NULL, 4 | surname VARCHAR (20) NOT NULL, 5 | credit_card_number VARCHAR(16) NOT NULL 6 | ); 7 | 8 | INSERT INTO customers ( 9 | name, 10 | surname, 11 | credit_card_number 12 | ) VALUES ( 13 | 'alice', 14 | 'smith', 15 | '1234567890123456' 16 | ), ( 17 | 'bob', 18 | 'green', 19 | '1234567890123456' 20 | ), 21 | ( 22 | 'frank', 23 | 'white', 24 | '1234567890123456' 25 | ); 26 | 27 | -------------------------------------------------------------------------------- /src/shared/export-solution.js: -------------------------------------------------------------------------------- 1 | // Ignore the following code if you're doing the exercise 2 | // This code conditionally loads either the user supplied solution 3 | // or the solved exercise depending on an environment variable 4 | // It is used so we can verify in CI that the provided solutions of the workshop work 5 | 6 | import { env } from './env.js' 7 | 8 | export function getSolutionToExport(userSolution, actualSolution) { 9 | if (env.USE_SOLUTION) { 10 | return actualSolution 11 | } 12 | return userSolution 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/env.js: -------------------------------------------------------------------------------- 1 | import { join } from 'desm' 2 | import envSchema from 'env-schema' 3 | import { Type } from '@sinclair/typebox' 4 | 5 | const EnvSchema = Type.Object({ 6 | PG_CONNECTION_STRING: Type.String(), 7 | JWT_SECRET: Type.String(), 8 | LOG_LEVEL: Type.String(), 9 | PRETTY_PRINT: Type.Boolean(), 10 | PRINT_ROUTES: Type.Boolean(), 11 | USE_SOLUTION: Type.Boolean(), 12 | COOKIES_SECRET: Type.String() 13 | }) 14 | 15 | export const env = envSchema({ 16 | schema: EnvSchema, 17 | dotenv: { path: join(import.meta.url, '.env') } 18 | }) 19 | -------------------------------------------------------------------------------- /src/shared/plugins/authenticate.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin' 2 | 3 | async function authenticate(fastify, options) { 4 | fastify.register(import('@fastify/jwt'), { 5 | secret: options.JWT_SECRET 6 | }) 7 | 8 | fastify.decorate('authenticate', async (req, reply) => { 9 | try { 10 | await req.jwtVerify() 11 | } catch (err) { 12 | reply.send(err) 13 | } 14 | }) 15 | } 16 | 17 | authenticate[Symbol.for('skip-override')] = true 18 | 19 | export default fp(authenticate, { 20 | fastify: '5.x', 21 | name: 'authenticate' 22 | }) 23 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: actions/setup-node@v6 17 | with: 18 | node-version-file: .nvmrc 19 | - run: npm ci 20 | - run: npm run build -- --base /owasp-top-ten-workshop/ 21 | - uses: peaceiris/actions-gh-pages@v4 22 | with: 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | publish_dir: dist 25 | -------------------------------------------------------------------------------- /src/a06-vulnerable-outdated/routes/profile/solution.js: -------------------------------------------------------------------------------- 1 | import { request } from 'undici' 2 | import errors from 'http-errors' 3 | 4 | export default function (fastify) { 5 | fastify.get( 6 | '/profile', 7 | { 8 | onRequest: [fastify.authenticate] 9 | }, 10 | async req => { 11 | const { username } = req.query 12 | const { body, statusCode } = await request({ 13 | origin: 'http://example.com', 14 | pathname: username 15 | }) 16 | if (statusCode !== 200) { 17 | throw errors.NotFound() 18 | } 19 | return body 20 | } 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/a03-injection/routes/customer/solution.js: -------------------------------------------------------------------------------- 1 | import SQL from '@nearform/sql' 2 | import errors from 'http-errors' 3 | 4 | export default async function customer(fastify) { 5 | fastify.get( 6 | '/customer', 7 | { 8 | onRequest: [fastify.authenticate] 9 | }, 10 | async req => { 11 | const { name } = req.query 12 | const { rows: customers } = await fastify.pg.query( 13 | SQL`SELECT * FROM customers WHERE name=${name}` // SQL function from @nearform/sql 14 | ) 15 | if (!customers.length) throw errors.NotFound() 16 | return customers 17 | } 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/check-linked-issues.yml: -------------------------------------------------------------------------------- 1 | name: Check linked issues 2 | 'on': 3 | pull_request_target: 4 | types: 5 | - opened 6 | - edited 7 | - reopened 8 | - synchronize 9 | jobs: 10 | check_pull_requests: 11 | runs-on: ubuntu-latest 12 | name: Check linked issues 13 | steps: 14 | - uses: nearform-actions/github-action-check-linked-issues@v1 15 | id: check-linked-issues 16 | with: 17 | github-token: ${{ secrets.GITHUB_TOKEN }} 18 | exclude-branches: release/**, dependabot/** 19 | permissions: 20 | issues: read 21 | pull-requests: write 22 | -------------------------------------------------------------------------------- /src/a10-server-side-request-forgery/routes/user/index.js: -------------------------------------------------------------------------------- 1 | import { getSolutionToExport } from 'owasp-shared/export-solution.js' 2 | import solution from './solution.js' 3 | import axios from 'axios' 4 | import errors from 'http-errors' 5 | 6 | function profilePicture(fastify) { 7 | fastify.post( 8 | '/user/image', 9 | { 10 | onRequest: [fastify.authenticate] 11 | }, 12 | async req => { 13 | const imgUrl = req.body.imgUrl 14 | const { data, status } = await axios.get(imgUrl) 15 | if (status !== 200) throw errors.BadRequest() 16 | return data 17 | } 18 | ) 19 | } 20 | 21 | export default getSolutionToExport(profilePicture, solution) 22 | -------------------------------------------------------------------------------- /src/a02-cryptographic-failure/utils/crypto.js: -------------------------------------------------------------------------------- 1 | import md5 from 'md5' 2 | import { getSolutionToExport } from 'owasp-shared/export-solution.js' 3 | import * as solution from './solution.js' 4 | 5 | let hashPassword = async function hashPassword(password) { 6 | return md5(password) 7 | } 8 | 9 | let comparePassword = async function comparePassword(password, hash) { 10 | return md5(password) === hash 11 | } 12 | 13 | // Note: This helper just helps with internal unit testing 14 | hashPassword = getSolutionToExport(hashPassword, solution.hashPassword) 15 | comparePassword = getSolutionToExport(comparePassword, solution.comparePassword) 16 | 17 | export { hashPassword, comparePassword } 18 | -------------------------------------------------------------------------------- /src/a09-security-logging/routes/profile/index.js: -------------------------------------------------------------------------------- 1 | import { getSolutionToExport } from 'owasp-shared/export-solution.js' 2 | import { request } from 'undici-5.8.0' 3 | import solution from './solution.js' 4 | 5 | async function profile(fastify) { 6 | fastify.get( 7 | '/profile', 8 | { 9 | onRequest: [fastify.authenticate] 10 | }, 11 | async req => { 12 | const { body } = await request('http://localhost:3001', { 13 | method: 'GET', 14 | headers: { 15 | 'content-type': req.headers['content-type'] 16 | } 17 | }) 18 | return body 19 | } 20 | ) 21 | } 22 | 23 | export default getSolutionToExport(profile, solution) 24 | -------------------------------------------------------------------------------- /src/a08-software-data-integrity-failures/routes/profile/index.js: -------------------------------------------------------------------------------- 1 | import solution from './solution.js' 2 | import { getSolutionToExport } from 'owasp-shared/export-solution.js' 3 | import serialize from 'node-serialize' 4 | 5 | function profile(fastify) { 6 | fastify.get('/profile', req => { 7 | const cookieAsStr = Buffer.from(req.cookies.profile, 'base64').toString( 8 | 'ascii' 9 | ) 10 | 11 | const profile = serialize.unserialize(cookieAsStr) 12 | 13 | if (profile.username) { 14 | return 'Hello ' + profile.username 15 | } 16 | 17 | return 'Hello guest' 18 | }) 19 | } 20 | 21 | // Note: This helper just helps with internal unit testing 22 | export default getSolutionToExport(profile, solution) 23 | -------------------------------------------------------------------------------- /src/a03-injection/routes/customer/index.js: -------------------------------------------------------------------------------- 1 | import solution from './solution.js' 2 | import { getSolutionToExport } from 'owasp-shared/export-solution.js' 3 | import errors from 'http-errors' 4 | 5 | async function customer(fastify) { 6 | fastify.get( 7 | '/customer', 8 | { 9 | onRequest: [fastify.authenticate] 10 | }, 11 | async req => { 12 | const { name } = req.query 13 | const { rows: customers } = await fastify.pg.query( 14 | `SELECT * FROM customers WHERE name='${name}'` 15 | ) 16 | if (!customers.length) throw errors.NotFound() 17 | return customers 18 | } 19 | ) 20 | } 21 | 22 | // Note: This helper just helps with internal unit testing 23 | export default getSolutionToExport(customer, solution) 24 | -------------------------------------------------------------------------------- /src/a06-vulnerable-outdated/step6.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import assert from 'node:assert/strict' 3 | import { step6Server } from './server.js' 4 | import { authHeaders } from 'owasp-shared' 5 | 6 | test('A06: Vulnerable and outdated components', async t => { 7 | let fastify 8 | 9 | t.beforeEach(async () => { 10 | fastify = await step6Server() 11 | }) 12 | 13 | t.afterEach(() => fastify.close()) 14 | 15 | await t.test(`SSRF should be not exploitable`, async () => { 16 | const res = await fastify.inject({ 17 | url: '/profile', 18 | method: 'GET', 19 | headers: authHeaders, 20 | query: { 21 | username: '//127.0.0.1' 22 | } 23 | }) 24 | 25 | assert.equal(res.statusCode, 404) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/a06-vulnerable-outdated/routes/profile/index.js: -------------------------------------------------------------------------------- 1 | import { getSolutionToExport } from 'owasp-shared/export-solution.js' 2 | import { request } from 'undici-5.8.0' 3 | import solution from './solution.js' 4 | import errors from 'http-errors' 5 | 6 | async function profile(fastify) { 7 | fastify.get( 8 | '/profile', 9 | { 10 | onRequest: [fastify.authenticate] 11 | }, 12 | async req => { 13 | const { username } = req.query 14 | const { body, statusCode } = await request({ 15 | origin: 'http://example.com', 16 | pathname: username 17 | }) 18 | if (statusCode !== 200) { 19 | throw errors.NotFound() 20 | } 21 | return body 22 | } 23 | ) 24 | } 25 | 26 | export default getSolutionToExport(profile, solution) 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | semver: 6 | description: The semver to use 7 | required: true 8 | default: patch 9 | type: choice 10 | options: 11 | - patch 12 | - minor 13 | - major 14 | pull_request: 15 | types: [closed] 16 | 17 | jobs: 18 | release: 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: write 22 | issues: write 23 | pull-requests: write 24 | steps: 25 | - uses: nearform-actions/optic-release-automation-action@v4 26 | with: 27 | github-token: ${{ secrets.github_token }} 28 | semver: ${{ github.event.inputs.semver }} 29 | commit-message: 'chore: release {version}' 30 | build-command: npm ci 31 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | img { 2 | width: 100%; 3 | } 4 | 5 | #slide-empty-center-page { 6 | text-align: center; 7 | } 8 | 9 | #slide-empty-center-page img { 10 | width: 50%; 11 | } 12 | 13 | #slide-using-inspector p img { 14 | width: 80%; 15 | } 16 | 17 | #slide-clinic p { 18 | text-align: center; 19 | } 20 | 21 | #slide-clinic p img { 22 | width: 50% !important; 23 | } 24 | 25 | #slide-deopt p { 26 | text-align: center; 27 | } 28 | 29 | #slide-deopt p img { 30 | width: 90%; 31 | } 32 | 33 | .no-border h1 { 34 | border-bottom: none; 35 | padding-top: 20%; 36 | text-align: center; 37 | } 38 | 39 | ul, 40 | ol { 41 | font-size: 1.75rem; 42 | } 43 | 44 | #slide-clinic-flame-img img, 45 | #slide-clinic-flame-img-zoomed img { 46 | height: 80%; 47 | } 48 | 49 | #slide-shapes-image img, 50 | #slide-shapes-transitions-image img { 51 | height: 25rem; 52 | } 53 | -------------------------------------------------------------------------------- /src/a09-security-logging/routes/profile/solution.js: -------------------------------------------------------------------------------- 1 | import { request } from 'undici' 2 | import errors from 'http-errors' 3 | 4 | export default function profile(fastify) { 5 | fastify.get( 6 | '/profile', 7 | { 8 | onRequest: [fastify.authenticate] 9 | }, 10 | async req => { 11 | console.log({ 12 | username: req.user.username, 13 | input: req.headers['content-type'] 14 | }) 15 | 16 | const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ 17 | 18 | if (headerCharRegex.exec(req.headers['content-type']) !== null) { 19 | throw errors.BadRequest() 20 | } 21 | 22 | const { body } = await request('http://localhost:3001', { 23 | method: 'GET', 24 | headers: { 25 | 'content-type': req.headers['content-type'] 26 | } 27 | }) 28 | return body 29 | } 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/a08-software-data-integrity-failures/step8.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import assert from 'node:assert/strict' 3 | import { step8Server } from './server.js' 4 | 5 | test('A08: Software and Data Integrity Failures', async t => { 6 | let fastify 7 | 8 | t.beforeEach(async () => { 9 | fastify = await step8Server() 10 | }) 11 | 12 | t.afterEach(() => fastify.close()) 13 | 14 | await t.test(`Should parse request cookie properly`, async () => { 15 | const cookieWithMaliciousCode = 16 | 'profile=eyJpZCI6MSwidXNlcm5hbWUiOiJfJCRORF9GVU5DJCRfZnVuY3Rpb24gKCkge1xuICAgIHRocm93IG5ldyBFcnJvcignc2VydmVyIGVycm9yJylcbiAgfSgpIn0=' 17 | 18 | const res = await fastify.inject({ 19 | url: '/profile', 20 | method: 'GET', 21 | headers: { 22 | Cookie: cookieWithMaliciousCode 23 | } 24 | }) 25 | 26 | assert.equal(res.statusCode, 200) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/a01-access-control/routes/profile/index.js: -------------------------------------------------------------------------------- 1 | import errors from 'http-errors' 2 | import SQL from '@nearform/sql' 3 | import solution from './solution.js' 4 | import { getSolutionToExport } from 'owasp-shared/export-solution.js' 5 | 6 | function profile(fastify) { 7 | fastify.get( 8 | '/profile', 9 | { 10 | onRequest: [fastify.authenticate] 11 | }, 12 | async req => { 13 | if (!req.user) { 14 | throw new errors.Unauthorized() 15 | } 16 | const { username } = req.query 17 | const { 18 | rows: [user] 19 | } = await fastify.pg.query( 20 | SQL`SELECT id, username, age FROM users WHERE username = ${username}` 21 | ) 22 | 23 | if (!user) { 24 | throw new errors.NotFound() 25 | } 26 | return user 27 | } 28 | ) 29 | } 30 | 31 | // Note: This helper just helps with internal unit testing 32 | export default getSolutionToExport(profile, solution) 33 | -------------------------------------------------------------------------------- /src/a10-server-side-request-forgery/routes/user/solution.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import errors from 'http-errors' 3 | import SQL from '@nearform/sql' 4 | 5 | export default async function profilePicture(fastify) { 6 | fastify.post( 7 | '/user/image', 8 | { 9 | onRequest: [fastify.authenticate] 10 | }, 11 | async req => { 12 | const { imgUrl } = req.body 13 | const url = validateUrl(imgUrl) 14 | const { 15 | rows: [whitelisted] 16 | } = await fastify.pg.query( 17 | SQL`SELECT * FROM allowedImageDomain WHERE hostname = ${url.hostname}` 18 | ) 19 | if (!whitelisted) { 20 | throw errors.Forbidden() 21 | } 22 | const { data } = await axios.get(url.href) 23 | return data 24 | } 25 | ) 26 | } 27 | 28 | function validateUrl(imgUrl) { 29 | try { 30 | return new URL(imgUrl) 31 | } catch (error) { 32 | throw error.BadRequest() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/shared/start-server.js: -------------------------------------------------------------------------------- 1 | import Fastify from 'fastify' 2 | 3 | export async function startServer(fastify) { 4 | console.log('Starting server') 5 | try { 6 | console.log('Listening on port 3000') 7 | await fastify.listen({ port: 3000 }) 8 | console.log('Fastify server is running') 9 | } catch (err) { 10 | console.error(err) 11 | fastify.log.error(err) 12 | process.exit(1) 13 | } 14 | } 15 | 16 | export async function startTargetServer(spy) { 17 | const fastify = Fastify() 18 | try { 19 | fastify.get('/', () => { 20 | return { message: 'Hello world!' } 21 | }) 22 | 23 | fastify.get('/secret', () => { 24 | spy() 25 | const message = 'something suspicious is happening' 26 | console.log(message) 27 | return { message } 28 | }) 29 | await fastify.listen({ port: 3001 }) 30 | return fastify 31 | } catch (err) { 32 | console.error(err) 33 | fastify.log.error(err) 34 | process.exit(1) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/a01-access-control/routes/profile/solution.js: -------------------------------------------------------------------------------- 1 | import errors from 'http-errors' 2 | import SQL from '@nearform/sql' 3 | 4 | export default async function solution(fastify) { 5 | fastify.get( 6 | '/profile', 7 | { 8 | onRequest: [fastify.authenticate] 9 | }, 10 | async req => { 11 | if (!req.user) { 12 | throw new errors.Unauthorized() 13 | } 14 | // We get the username from the logged in user, not from the query 15 | const username = req.user.username 16 | // if the query username does not match with the user's one, return a 403 Forbidden error 17 | if (username !== req.query.username) { 18 | throw new errors.Forbidden() 19 | } 20 | const { 21 | rows: [user] 22 | } = await fastify.pg.query( 23 | SQL`SELECT id, username, age FROM users WHERE username = ${username}` 24 | ) 25 | 26 | if (!user) { 27 | throw new errors.NotFound() 28 | } 29 | return user 30 | } 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/a02-cryptographic-failure/routes/changePassword.js: -------------------------------------------------------------------------------- 1 | import errors from 'http-errors' 2 | import { Type } from '@sinclair/typebox' 3 | import SQL from '@nearform/sql' 4 | import { hashPassword } from '../utils/crypto.js' 5 | 6 | const schema = { 7 | body: Type.Object({ 8 | password: Type.String() 9 | }) 10 | } 11 | 12 | export default async function changePassword(fastify) { 13 | fastify.post( 14 | '/change-password', 15 | { 16 | schema, 17 | onRequest: [fastify.authenticate] 18 | }, 19 | async req => { 20 | const { username } = req.user 21 | const { password } = req.body 22 | const hashedPassword = await hashPassword(password) 23 | const { 24 | rows: [user] 25 | } = await fastify.pg.query( 26 | SQL`UPDATE users SET password = ${hashedPassword} WHERE username = ${username} RETURNING id, username` 27 | ) 28 | if (!user) { 29 | throw errors.InternalServerError() 30 | } 31 | return {} 32 | } 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/a03-injection/step3.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import assert from 'node:assert/strict' 3 | import { step3Server } from './server.js' 4 | import { authHeaders } from 'owasp-shared' 5 | 6 | test('A03: Injection', async t => { 7 | let fastify 8 | 9 | t.beforeEach(async () => { 10 | fastify = await step3Server() 11 | }) 12 | 13 | t.afterEach(() => fastify.close()) 14 | 15 | await t.test(`retrieves user correctly`, async () => { 16 | const res = await fastify.inject({ 17 | url: '/customer', 18 | method: 'GET', 19 | headers: authHeaders, 20 | query: { 21 | name: 'alice' 22 | } 23 | }) 24 | 25 | assert.equal(res.statusCode, 200) 26 | }) 27 | 28 | await t.test(`doesn't allow sql injection`, async () => { 29 | const res = await fastify.inject({ 30 | url: '/customer', 31 | method: 'GET', 32 | headers: authHeaders, 33 | query: { 34 | name: `' OR '1'='1` 35 | } 36 | }) 37 | 38 | assert.equal(res.statusCode, 404) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/a09-security-logging/step9.test.js: -------------------------------------------------------------------------------- 1 | import { authHeaders, startTargetServer } from 'owasp-shared' 2 | import { test } from 'node:test' 3 | import assert from 'node:assert/strict' 4 | import { step9Server } from './server.js' 5 | 6 | test('A09: Security Logging', async t => { 7 | let fastify 8 | let targetServer 9 | const spy = t.mock.fn() 10 | 11 | t.before(async () => { 12 | targetServer = await startTargetServer(spy) 13 | }) 14 | 15 | t.beforeEach(async () => { 16 | fastify = await step9Server() 17 | }) 18 | 19 | t.afterEach(() => { 20 | fastify.close() 21 | }) 22 | 23 | t.after(() => { 24 | targetServer.close() 25 | }) 26 | 27 | await t.test(`CRLF should be not exploitable`, async () => { 28 | const res = await fastify.inject({ 29 | url: '/profile', 30 | method: 'GET', 31 | headers: { 32 | ...authHeaders, 33 | 'content-type': 'application/json\r\n\r\nGET /secret HTTP/1.1' 34 | } 35 | }) 36 | 37 | assert.equal(res.statusCode, 400) 38 | assert.equal(spy.mock.callCount(), 0) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | test: 9 | name: Lint and test 10 | runs-on: ubuntu-latest 11 | services: 12 | postgres: 13 | image: postgres:alpine 14 | env: 15 | POSTGRES_PASSWORD: owasp10 16 | ports: 17 | - 5434:5432 18 | options: >- 19 | --health-cmd pg_isready 20 | --health-interval 10s 21 | --health-timeout 5s 22 | --health-retries 5 23 | steps: 24 | - uses: actions/checkout@v6 25 | - uses: actions/setup-node@v6 26 | with: 27 | node-version-file: .nvmrc 28 | - run: | 29 | npm ci 30 | npm run db:migrate 31 | npm run lint 32 | npm test 33 | automerge: 34 | name: Merge dependabot's PRs 35 | needs: test 36 | runs-on: ubuntu-latest 37 | permissions: 38 | pull-requests: write 39 | contents: write 40 | steps: 41 | - uses: fastify/github-action-merge-dependabot@v3 42 | -------------------------------------------------------------------------------- /src/a01-access-control/step1.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import assert from 'node:assert/strict' 3 | import { authHeaders } from 'owasp-shared' 4 | import { step1Server } from './server.js' 5 | 6 | test('A01: Access Control', async t => { 7 | let fastify 8 | 9 | t.beforeEach(async () => { 10 | fastify = await step1Server() 11 | }) 12 | 13 | t.afterEach(() => fastify.close()) 14 | 15 | await t.test(`doesn't return anything if not logged in`, async () => { 16 | const res = await fastify.inject({ 17 | url: '/profile', 18 | method: 'GET', 19 | query: { 20 | username: 'bob' 21 | } 22 | }) 23 | 24 | assert.equal(res.statusCode, 401) 25 | }) 26 | 27 | await t.test( 28 | `doesn't return another user's info when changing get parameters`, 29 | async () => { 30 | const res = await fastify.inject({ 31 | url: '/profile', 32 | method: 'GET', 33 | headers: authHeaders, 34 | query: { 35 | username: 'bob' 36 | } 37 | }) 38 | 39 | assert.equal(res.statusCode, 403) 40 | } 41 | ) 42 | }) 43 | -------------------------------------------------------------------------------- /src/shared/routes/login.js: -------------------------------------------------------------------------------- 1 | import errors from 'http-errors' 2 | import { Type } from '@sinclair/typebox' 3 | import SQL from '@nearform/sql' 4 | import { comparePassword } from '../../a02-cryptographic-failure/utils/crypto.js' 5 | 6 | const schema = { 7 | body: Type.Object({ 8 | username: Type.String(), 9 | password: Type.String() 10 | }), 11 | response: { 12 | 200: Type.Object({ 13 | token: Type.String() 14 | }) 15 | } 16 | } 17 | 18 | export default async function login(fastify) { 19 | fastify.post('/login', { schema }, async req => { 20 | const { username, password } = req.body 21 | 22 | const { 23 | rows: [user] 24 | } = await fastify.pg.query( 25 | SQL`SELECT id, username, password FROM users WHERE username = ${username}` 26 | ) 27 | 28 | if (!user) { 29 | throw errors.Unauthorized('No matching user found') 30 | } 31 | const passwordMatch = await comparePassword(password, user.password) 32 | if (!passwordMatch) { 33 | throw errors.Unauthorized('Invalid Password') 34 | } 35 | return { token: fastify.jwt.sign({ id: user.id, username: user.username }) } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/a02-cryptographic-failure/step2.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import assert from 'node:assert/strict' 3 | import { authHeaders } from 'owasp-shared' 4 | import { step2Server } from './server.js' 5 | 6 | test('A02: Cryptographic Failure', async t => { 7 | let fastify 8 | 9 | t.beforeEach(async () => { 10 | fastify = await step2Server() 11 | }) 12 | 13 | t.afterEach(() => fastify.close()) 14 | 15 | await t.test(`can change Alice's password`, async () => { 16 | const res = await fastify.inject({ 17 | url: '/change-password', 18 | method: 'POST', 19 | headers: authHeaders, 20 | body: { 21 | password: 'newpassword' 22 | } 23 | }) 24 | 25 | assert.equal(res.statusCode, 200) 26 | }) 27 | 28 | await t.test(`Password isn't hashed with weak md5`, async () => { 29 | const res = await fastify.inject({ 30 | url: '/all-data', 31 | method: 'GET', 32 | headers: authHeaders 33 | }) 34 | 35 | assert.equal(res.statusCode, 200) 36 | 37 | const accounts = res.json() 38 | const alice = accounts.find(account => account.username === 'alice') 39 | const hashedPassword = alice.password 40 | 41 | assert.doesNotMatch(hashedPassword, /5e9d11a14ad1c8dd77e98ef9b53fd1ba/) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /src/shared/routes/register.js: -------------------------------------------------------------------------------- 1 | import errors from 'http-errors' 2 | import { Type } from '@sinclair/typebox' 3 | import SQL from '@nearform/sql' 4 | import { faker } from '@faker-js/faker' 5 | import { hashPassword } from '../../a02-cryptographic-failure/utils/crypto.js' 6 | 7 | const schema = { 8 | body: Type.Object({ 9 | username: Type.String(), 10 | password: Type.String() 11 | }), 12 | response: { 13 | 200: Type.Object({ 14 | token: Type.String() 15 | }) 16 | } 17 | } 18 | 19 | export default async function register(fastify) { 20 | fastify.post('/register', { schema }, async req => { 21 | const { username, password } = req.body 22 | const birthDate = faker.date.past() 23 | const hashedPassword = await hashPassword(password) 24 | const creditCardNumber = faker.finance.creditCardNumber() 25 | const { 26 | rows: [user] 27 | } = await fastify.pg.query( 28 | SQL`INSERT INTO users (username, password, age, credit_card_number) VALUES (${username}, ${hashedPassword}, ${birthDate.toISOString()}, ${creditCardNumber}) RETURNING id, username` 29 | ) 30 | 31 | if (!user) { 32 | throw errors.InternalServerError() 33 | } 34 | return { token: fastify.jwt.sign({ id: user.id, username: user.username }) } 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/a04-insecure-design/step4.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import assert from 'node:assert/strict' 3 | import { step4Server } from './server.js' 4 | 5 | test('A04: Insecure Design', async t => { 6 | let fastify 7 | 8 | t.beforeEach(async () => { 9 | fastify = await step4Server() 10 | }) 11 | 12 | t.afterEach(() => fastify.close()) 13 | 14 | await t.test( 15 | `doesn't allow too many requests in a row within the rate limit`, 16 | async t => { 17 | t.mock.timers.enable() 18 | 19 | let res 20 | 21 | res = await fastify.inject({ url: '/buy-product', method: 'POST' }) 22 | assert.equal(res.statusCode, 200) 23 | 24 | res = await fastify.inject({ url: '/buy-product', method: 'POST' }) 25 | assert.equal(res.statusCode, 200) 26 | 27 | res = await fastify.inject({ url: '/buy-product', method: 'POST' }) 28 | assert.equal(res.statusCode, 429) // after two attempts within one minute, the user is blocked by rate limit 29 | 30 | t.mock.timers.tick(1100 * 60) //time progresses a little over a minute forward 31 | 32 | res = await fastify.inject({ url: '/buy-product', method: 'POST' }) 33 | 34 | assert.equal(res.statusCode, 200) // user can make a request again 35 | 36 | t.after(() => { 37 | t.mock.reset() 38 | }) 39 | } 40 | ) 41 | }) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The OWASP Top Ten Workshop 2 | 3 | ![CI](https://github.com/nearform/owasp-top-ten-workshop/actions/workflows/ci.yml/badge.svg?event=push) 4 | 5 | Workshop based on the [OWASP Top Ten](https://owasp.org/www-project-top-ten/) security vulnerabilities 6 | 7 | ## Requirements 8 | 9 | - Node LTS 10 | - docker 11 | - docker-compose 12 | - Postman for testing requests 13 | 14 | ## Setup 15 | 16 | - `npm ci` 17 | - `npm run db:up` 18 | - `npm run db:migrate` 19 | 20 | ## Slides 21 | 22 | Slides contain instructions for the workshop. You can read them at https://nearform.github.io/owasp-top-ten-workshop, or: 23 | 24 | `npm start` will open the slides in the browser 25 | 26 | ## Running an exercise 27 | 28 | ```bash 29 | cd src/a01-access-control 30 | npm start 31 | ``` 32 | 33 | ## Verifying an exercise solution 34 | 35 | This will run automated tests that fail until the issue in the exercise has been solved 36 | 37 | (Some steps of the workshop might not have automated tests) 38 | 39 | ```bash 40 | cd src/a01-access-control 41 | npm run verify 42 | ``` 43 | 44 | ## Run exercise verification tests on a single project 45 | 46 | - `npm run verify -w src/a01-access-control` 47 | 48 | [![banner](https://raw.githubusercontent.com/nearform/.github/refs/heads/master/assets/os-banner-green.svg)](https://www.nearform.com/contact/?utm_source=open-source&utm_medium=banner&utm_campaign=os-project-pages) 49 | -------------------------------------------------------------------------------- /src/a07-authentication-failures/step7.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import assert from 'node:assert/strict' 3 | import { step7Server } from './server.js' 4 | import { faker } from '@faker-js/faker' 5 | 6 | test('A07: Identification and Authentication Failures:', async t => { 7 | let fastify 8 | 9 | t.beforeEach(async () => { 10 | fastify = await step7Server() 11 | }) 12 | 13 | t.afterEach(() => fastify.close()) 14 | 15 | await t.test(`non leaked password`, async () => { 16 | const registerRes = await fastify.inject({ 17 | url: '/register', 18 | method: 'POST', 19 | body: { 20 | username: faker.internet.username(), 21 | password: 'N3v_3R-L3akEd' 22 | } 23 | }) 24 | 25 | assert.equal(registerRes.statusCode, 200) 26 | assert.ok(registerRes.json().token) 27 | }) 28 | 29 | await t.test(`leaked password`, async () => { 30 | const registerRes = await fastify.inject({ 31 | url: '/register', 32 | method: 'POST', 33 | body: { 34 | username: faker.internet.username(), 35 | password: 'L3Ak_3d-Lik3_N0-t0M0rr0W' 36 | } 37 | }) 38 | 39 | assert.equal(registerRes.statusCode, 400) 40 | assert.equal( 41 | registerRes.json().message, 42 | 'You are trying to use password that is known to be exposed in data breaches: adobe. Use a different one. Read more here: https://haveibeenpwned.com/Passwords.' 43 | ) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/a10-server-side-request-forgery/step10.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import assert from 'node:assert/strict' 3 | import { step10Server } from './server.js' 4 | import { authHeaders, startTargetServer } from 'owasp-shared' 5 | 6 | test('A10: Server side request forgery', async t => { 7 | let fastify 8 | let targetServer 9 | const spy = t.mock.fn() 10 | 11 | t.before(async () => { 12 | targetServer = await startTargetServer(spy) 13 | }) 14 | 15 | t.beforeEach(async () => { 16 | fastify = await step10Server() 17 | }) 18 | 19 | t.afterEach(() => { 20 | fastify.close() 21 | }) 22 | 23 | t.after(() => { 24 | targetServer.close() 25 | }) 26 | 27 | await t.test(`SSRF should be not exploitable`, async () => { 28 | const res = await fastify.inject({ 29 | url: '/user/image', 30 | method: 'POST', 31 | headers: authHeaders, 32 | body: { 33 | imgUrl: 'http://localhost:3001/secret' 34 | } 35 | }) 36 | 37 | assert.equal(res.statusCode, 403) 38 | assert.equal(spy.mock.callCount(), 0) 39 | }) 40 | 41 | await t.test(`Allow regular image website`, async () => { 42 | const res = await fastify.inject({ 43 | url: '/user/image', 44 | method: 'POST', 45 | headers: authHeaders, 46 | body: { 47 | imgUrl: 'https://i.imgflip.com/6upp1a.jpg' 48 | } 49 | }) 50 | 51 | assert.equal(res.statusCode, 200) 52 | assert.equal(spy.mock.callCount(), 0) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /src/a07-authentication-failures/routes/user/index.js: -------------------------------------------------------------------------------- 1 | import { register as solutionRegister } from './solution.js' 2 | import { getSolutionToExport } from 'owasp-shared/export-solution.js' 3 | import errors from 'http-errors' 4 | import { Type } from '@sinclair/typebox' 5 | import SQL from '@nearform/sql' 6 | import { faker } from '@faker-js/faker' 7 | import { hashPassword } from '../../../a02-cryptographic-failure/utils/crypto.js' 8 | 9 | const schema = { 10 | body: Type.Object({ 11 | username: Type.String(), 12 | password: Type.String() 13 | }), 14 | response: { 15 | 200: Type.Object({ 16 | token: Type.String() 17 | }) 18 | } 19 | } 20 | 21 | export default async function register(fastify) { 22 | fastify.post('/register', { schema }, async req => { 23 | const { username, password } = req.body 24 | const age = faker.number.int({ min: 12, max: 85 }) 25 | const hashedPassword = await hashPassword(password) 26 | const creditCardNumber = faker.finance.creditCardNumber('#'.repeat(16)) 27 | const { 28 | rows: [user] 29 | } = await fastify.pg.query( 30 | SQL`INSERT INTO users (username, password, age, credit_card_number) VALUES (${username}, ${hashedPassword}, ${age}, ${creditCardNumber}) RETURNING id, username` 31 | ) 32 | 33 | if (!user) { 34 | throw errors.InternalServerError() 35 | } 36 | return { token: fastify.jwt.sign({ id: user.id, username: user.username }) } 37 | }) 38 | } 39 | 40 | export const registerRoute = getSolutionToExport(register, solutionRegister) 41 | -------------------------------------------------------------------------------- /src/a05-security-misconfiguration/routes/user/solution.js: -------------------------------------------------------------------------------- 1 | import SQL from '@nearform/sql' 2 | import { Type } from '@sinclair/typebox' 3 | import errors from 'http-errors' 4 | import { comparePassword } from '../../../a02-cryptographic-failure/utils/solution.js' 5 | 6 | const schema = { 7 | body: Type.Object({ 8 | username: Type.String(), 9 | password: Type.String() 10 | }), 11 | response: { 12 | 200: Type.Object({ 13 | token: Type.String() 14 | }) 15 | } 16 | } 17 | 18 | export function login(fastify) { 19 | fastify.post('/login', { schema }, async (req, rep) => { 20 | const { username, password } = req.body 21 | const { 22 | rows: [user] 23 | } = await fastify.pg.query( 24 | SQL`SELECT id, username, password FROM users WHERE username = ${username}` 25 | ) 26 | if (!user) { 27 | throw errors.Unauthorized('No matching user found') 28 | } 29 | 30 | const passwordMatch = await comparePassword(password, user.password) 31 | if (!passwordMatch) { 32 | throw errors.Unauthorized('Invalid Password') 33 | } 34 | 35 | rep.setCookie('userId', JSON.stringify(user.id), { 36 | signed: true, 37 | httpOnly: true 38 | }) 39 | return 'user logged in' 40 | }) 41 | } 42 | 43 | export function profile(fastify) { 44 | fastify.get('/profile', async req => { 45 | const { value: id, valid } = fastify.unsignCookie( 46 | req?.cookies?.userId || '' 47 | ) 48 | 49 | if (!valid) { 50 | throw new errors.Unauthorized() 51 | } 52 | const { 53 | rows: [user] 54 | } = await fastify.pg.query( 55 | SQL`SELECT id, username, age FROM users WHERE id = ${id}` 56 | ) 57 | if (!user) { 58 | throw new errors.NotFound() 59 | } 60 | return user 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /src/a05-security-misconfiguration/step5.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import assert from 'node:assert/strict' 3 | import { step5Server } from './server.js' 4 | 5 | test('A05: Security Misconfiguration', async t => { 6 | let fastify 7 | 8 | t.beforeEach(async () => { 9 | fastify = await step5Server() 10 | }) 11 | 12 | t.afterEach(() => fastify.close()) 13 | 14 | await t.test(`cookie is signed`, async () => { 15 | const loginRes = await fastify.inject({ 16 | url: '/login', 17 | method: 'POST', 18 | body: { 19 | username: 'alice', 20 | password: 'newpassword' 21 | } 22 | }) 23 | const [cookie] = loginRes.cookies 24 | assert.equal(cookie.name, 'userId') 25 | assert.notEqual(cookie.value, '1') 26 | assert.equal(loginRes.statusCode, 200) 27 | }) 28 | 29 | await t.test(`cookie cannot be altered`, async () => { 30 | const res = await fastify.inject({ 31 | url: '/profile', 32 | method: 'GET', 33 | cookies: { 34 | userId: '2' 35 | } 36 | }) 37 | assert.notDeepEqual(res.json(), { 38 | username: 'bob', 39 | id: 2, 40 | age: 31 41 | }) 42 | assert.equal(res.statusCode, 401) 43 | }) 44 | 45 | await t.test(`cookie authentication flow`, async () => { 46 | const loginRes = await fastify.inject({ 47 | url: '/login', 48 | method: 'POST', 49 | body: { 50 | username: 'alice', 51 | password: 'newpassword' 52 | } 53 | }) 54 | const [cookie] = loginRes.cookies 55 | const res = await fastify.inject({ 56 | url: '/profile', 57 | method: 'GET', 58 | cookies: { 59 | [cookie.name]: cookie.value 60 | } 61 | }) 62 | 63 | assert.equal(res.statusCode, 200) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/shared/build-server.js: -------------------------------------------------------------------------------- 1 | import { join } from 'desm' 2 | import Fastify from 'fastify' 3 | import autoload from '@fastify/autoload' 4 | import fastifyPrintRoutes from 'fastify-print-routes' 5 | 6 | /** buildServer 7 | * @param {{ baseDir: string, env: Object, fastifyOptions: Object, autoloadPlugins: boolean, autoloadRoutes: boolean }} config 8 | */ 9 | export async function buildServer(config) { 10 | const opts = { 11 | ...config.fastifyOptions, 12 | logger: { 13 | level: config.env.LOG_LEVEL, 14 | ...(config.env.PRETTY_PRINT && { 15 | transport: { 16 | target: 'pino-pretty' 17 | } 18 | }) 19 | } 20 | } 21 | 22 | const fastify = Fastify(opts) 23 | 24 | if (config.env.PRINT_ROUTES) { 25 | await fastify.register(fastifyPrintRoutes) 26 | } 27 | 28 | fastify.register(import('@fastify/postgres'), { 29 | connectionString: config.env.PG_CONNECTION_STRING 30 | }) 31 | // Authentication plugin (imported manually for options typing) 32 | fastify.register(import('./plugins/authenticate.js'), config.env) 33 | 34 | if (config.autoloadPlugins) { 35 | fastify.register(autoload, { 36 | dir: join(config.baseDir, 'plugins'), 37 | options: opts 38 | }) 39 | } 40 | 41 | fastify.register(import('@fastify/cookie'), { 42 | secret: config.env.COOKIES_SECRET, 43 | parseOptions: {} 44 | }) 45 | 46 | if (config.autoloadRoutes) { 47 | fastify.register(autoload, { 48 | dir: join(config.baseDir, 'routes'), 49 | options: opts 50 | }) 51 | } 52 | 53 | if (!config.excludeSharedRoutes) { 54 | fastify.register(autoload, { 55 | // Shared routes 56 | dir: join(import.meta.url, 'routes'), 57 | options: opts 58 | }) 59 | } 60 | 61 | fastify.log.info('Fastify is starting up!') 62 | 63 | return fastify 64 | } 65 | -------------------------------------------------------------------------------- /src/a07-authentication-failures/routes/user/solution.js: -------------------------------------------------------------------------------- 1 | import { Type } from '@sinclair/typebox' 2 | import errors from 'http-errors' 3 | import SQL from '@nearform/sql' 4 | import { faker } from '@faker-js/faker' 5 | import { hashPassword } from '../../../a02-cryptographic-failure/utils/crypto.js' 6 | 7 | const schema = { 8 | body: Type.Object({ 9 | username: Type.String(), 10 | password: Type.String() 11 | }), 12 | response: { 13 | 200: Type.Object({ 14 | token: Type.String() 15 | }), 16 | 400: Type.Object({ 17 | message: Type.String() 18 | }) 19 | } 20 | } 21 | 22 | export function register(fastify) { 23 | fastify.post('/register', { schema }, async (req, res) => { 24 | const { username, password } = req.body 25 | const age = faker.number.int({ min: 12, max: 85 }) 26 | const hashedPassword = await hashPassword(password) 27 | const creditCardNumber = faker.finance.creditCardNumber('#'.repeat(16)) 28 | 29 | const { 30 | rows: [breach] 31 | } = await fastify.pg.query( 32 | SQL`SELECT * FROM databreachrecords WHERE password=${password}` 33 | ) 34 | 35 | if (breach) { 36 | res.send( 37 | errors.BadRequest( 38 | `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.` 39 | ) 40 | ) 41 | } 42 | 43 | const { 44 | rows: [user] 45 | } = await fastify.pg.query( 46 | SQL`INSERT INTO users (username, password, age, credit_card_number) VALUES (${username}, ${hashedPassword}, ${age}, ${creditCardNumber}) RETURNING id, username` 47 | ) 48 | 49 | if (!user) { 50 | throw errors.InternalServerError() 51 | } 52 | return { token: fastify.jwt.sign({ id: user.id, username: user.username }) } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /src/a05-security-misconfiguration/routes/user/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | login as solutionLogin, 3 | profile as solutionProfile 4 | } from './solution.js' 5 | import { getSolutionToExport } from 'owasp-shared/export-solution.js' 6 | import errors from 'http-errors' 7 | import { Type } from '@sinclair/typebox' 8 | import SQL from '@nearform/sql' 9 | import { comparePassword } from '../../../a02-cryptographic-failure/utils/solution.js' 10 | 11 | const schema = { 12 | body: Type.Object({ 13 | username: Type.String(), 14 | password: Type.String() 15 | }), 16 | response: { 17 | 200: Type.Object({ 18 | token: Type.String() 19 | }) 20 | } 21 | } 22 | 23 | function login(fastify) { 24 | fastify.post('/login', { schema }, async (req, rep) => { 25 | const { username, password } = req.body 26 | const { 27 | rows: [user] 28 | } = await fastify.pg.query( 29 | SQL`SELECT id, username, password FROM users WHERE username = ${username}` 30 | ) 31 | if (!user) { 32 | throw errors.Unauthorized('No matching user found') 33 | } 34 | const passwordMatch = await comparePassword(password, user.password) 35 | if (!passwordMatch) { 36 | throw errors.Unauthorized('Invalid Password') 37 | } 38 | 39 | rep.setCookie('userId', user.id, { signed: false }) 40 | return 'user logged in' 41 | }) 42 | } 43 | 44 | function profile(fastify) { 45 | fastify.get('/profile', async req => { 46 | if (!req.cookies?.userId) { 47 | throw new errors.Unauthorized() 48 | } 49 | const id = req.cookies.userId 50 | const { 51 | rows: [user] 52 | } = await fastify.pg.query( 53 | SQL`SELECT id, username, age FROM users WHERE id = ${id}` 54 | ) 55 | if (!user) { 56 | throw new errors.NotFound() 57 | } 58 | return user 59 | }) 60 | } 61 | 62 | export const loginRoute = getSolutionToExport(login, solutionLogin) 63 | export const profileRoute = getSolutionToExport(profile, solutionProfile) 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owasp-top-ten-workshop", 3 | "private": true, 4 | "type": "module", 5 | "workspaces": [ 6 | "src/*" 7 | ], 8 | "version": "1.0.0", 9 | "license": "CC-BY-SA-4.0", 10 | "author": "Liana Pigeot ", 11 | "description": "OWASP Top 10 Security Vulnerabilities Workshop", 12 | "main": "index.js", 13 | "imports": { 14 | "#/*": "src/*" 15 | }, 16 | "scripts": { 17 | "build": "slidev build", 18 | "start": "slidev --open", 19 | "export": "slidev export", 20 | "db:up": "docker-compose up -d", 21 | "db:migrate": "postgrator", 22 | "db:down": "docker-compose down", 23 | "verify": "npm run verify --workspaces --if-present", 24 | "test": "npm run test --workspaces --if-present", 25 | "lint": "eslint .", 26 | "prepare": "husky" 27 | }, 28 | "dependencies": { 29 | "@faker-js/faker": "^10.1.0", 30 | "@fastify/autoload": "^6.3.1", 31 | "@fastify/jwt": "^10.0.0", 32 | "@fastify/postgres": "^6.0.2", 33 | "@fastify/type-provider-typebox": "^6.1.0", 34 | "@nearform/sql": "^1.10.7", 35 | "@sinclair/typebox": "^0.34.40", 36 | "@slidev/cli": "^52.8.0", 37 | "@vueuse/shared": "^14.1.0", 38 | "bcrypt": "^6.0.0", 39 | "desm": "^1.3.1", 40 | "env-schema": "^7.0.0", 41 | "fastify": "^5.6.2", 42 | "fastify-plugin": "^5.1.0", 43 | "fastify-print-routes": "^5.0.1", 44 | "http-errors": "^2.0.1", 45 | "md5": "^2.3.0", 46 | "pg": "^8.16.3", 47 | "pino-pretty": "^13.1.2", 48 | "slidev-theme-nearform": "^2.1.0", 49 | "undici": "^7.16.0" 50 | }, 51 | "devDependencies": { 52 | "@commitlint/cli": "^20.1.0", 53 | "@commitlint/config-conventional": "^20.0.0", 54 | "@tsconfig/node16": "^16.1.8", 55 | "cross-env": "^10.1.0", 56 | "eslint": "^8.57.0", 57 | "eslint-config-prettier": "^10.1.8", 58 | "eslint-plugin-prettier": "^5.5.4", 59 | "eslint-plugin-sql": "^3.2.2", 60 | "husky": "^9.1.6", 61 | "lint-staged": "^16.2.7", 62 | "nodemon": "^3.1.11", 63 | "postgrator-cli": "^9.1.0", 64 | "prettier": "^3.7.4" 65 | }, 66 | "lint-staged": { 67 | "*.{js,jsx}": "eslint --cache --fix" 68 | }, 69 | "repository": { 70 | "type": "git", 71 | "url": "git+https://github.com/nearform/owasp-top-ten-workshop.git" 72 | }, 73 | "bugs": { 74 | "url": "https://github.com/nearform/owasp-top-ten-workshop/issues" 75 | }, 76 | "homepage": "https://github.com/nearform/owasp-top-ten-workshop#readme" 77 | } 78 | -------------------------------------------------------------------------------- /public/images/nearform.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 13 | 17 | 21 | 25 | 29 | 33 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/core/pull/3399 4 | import '@vue/runtime-core' 5 | 6 | export {} 7 | 8 | declare module '@vue/runtime-core' { 9 | export interface GlobalComponents { 10 | Arrow: typeof import('./node_modules/@slidev/client/builtin/Arrow.vue')['default'] 11 | AutoFitText: typeof import('./node_modules/@slidev/client/builtin/AutoFitText.vue')['default'] 12 | 'Carbon:account': typeof import('~icons/carbon/account')['default'] 13 | 'Carbon:alignBoxBottomRight': typeof import('~icons/carbon/align-box-bottom-right')['default'] 14 | 'Carbon:apps': typeof import('~icons/carbon/apps')['default'] 15 | 'Carbon:arrowLeft': typeof import('~icons/carbon/arrow-left')['default'] 16 | 'Carbon:arrowRight': typeof import('~icons/carbon/arrow-right')['default'] 17 | 'Carbon:arrowUpRight': typeof import('~icons/carbon/arrow-up-right')['default'] 18 | 'Carbon:checkbox': typeof import('~icons/carbon/checkbox')['default'] 19 | 'Carbon:checkmark': typeof import('~icons/carbon/checkmark')['default'] 20 | 'Carbon:chevronUp': typeof import('~icons/carbon/chevron-up')['default'] 21 | 'Carbon:close': typeof import('~icons/carbon/close')['default'] 22 | 'Carbon:closeOutline': typeof import('~icons/carbon/close-outline')['default'] 23 | 'Carbon:delete': typeof import('~icons/carbon/delete')['default'] 24 | 'Carbon:download': typeof import('~icons/carbon/download')['default'] 25 | 'Carbon:error': typeof import('~icons/carbon/error')['default'] 26 | 'Carbon:information': typeof import('~icons/carbon/information')['default'] 27 | 'Carbon:launch': typeof import('~icons/carbon/launch')['default'] 28 | 'Carbon:maximize': typeof import('~icons/carbon/maximize')['default'] 29 | 'Carbon:minimize': typeof import('~icons/carbon/minimize')['default'] 30 | 'Carbon:pen': typeof import('~icons/carbon/pen')['default'] 31 | 'Carbon:pin': typeof import('~icons/carbon/pin')['default'] 32 | 'Carbon:pinFilled': typeof import('~icons/carbon/pin-filled')['default'] 33 | 'Carbon:presentationFile': typeof import('~icons/carbon/presentation-file')['default'] 34 | 'Carbon:radioButton': typeof import('~icons/carbon/radio-button')['default'] 35 | 'Carbon:redo': typeof import('~icons/carbon/redo')['default'] 36 | 'Carbon:renew': typeof import('~icons/carbon/renew')['default'] 37 | 'Carbon:settingsAdjust': typeof import('~icons/carbon/settings-adjust')['default'] 38 | 'Carbon:stopOutline': typeof import('~icons/carbon/stop-outline')['default'] 39 | 'Carbon:textAnnotationToggle': typeof import('~icons/carbon/text-annotation-toggle')['default'] 40 | 'Carbon:time': typeof import('~icons/carbon/time')['default'] 41 | 'Carbon:undo': typeof import('~icons/carbon/undo')['default'] 42 | 'Carbon:userAvatar': typeof import('~icons/carbon/user-avatar')['default'] 43 | 'Carbon:userSpeaker': typeof import('~icons/carbon/user-speaker')['default'] 44 | 'Carbon:video': typeof import('~icons/carbon/video')['default'] 45 | CarbonMoon: typeof import('~icons/carbon/moon')['default'] 46 | CarbonSun: typeof import('~icons/carbon/sun')['default'] 47 | CodeBlockWrapper: typeof import('./node_modules/@slidev/client/builtin/CodeBlockWrapper.vue')['default'] 48 | Link: typeof import('./node_modules/@slidev/client/builtin/Link.vue')['default'] 49 | Mermaid: typeof import('./node_modules/@slidev/client/builtin/Mermaid.vue')['default'] 50 | Monaco: typeof import('./node_modules/@slidev/client/builtin/Monaco.vue')['default'] 51 | PhCheckCircle: typeof import('~icons/ph/check-circle')['default'] 52 | PhClipboard: typeof import('~icons/ph/clipboard')['default'] 53 | PhCursorDuotone: typeof import('~icons/ph/cursor-duotone')['default'] 54 | PhCursorFill: typeof import('~icons/ph/cursor-fill')['default'] 55 | PlantUml: typeof import('./node_modules/@slidev/client/builtin/PlantUml.vue')['default'] 56 | RenderWhen: typeof import('./node_modules/@slidev/client/builtin/RenderWhen.vue')['default'] 57 | RouterLink: typeof import('vue-router')['RouterLink'] 58 | RouterView: typeof import('vue-router')['RouterView'] 59 | SlideCurrentNo: typeof import('./node_modules/@slidev/client/builtin/SlideCurrentNo.vue')['default'] 60 | SlidesTotal: typeof import('./node_modules/@slidev/client/builtin/SlidesTotal.vue')['default'] 61 | Starport: typeof import('vue-starport')['Starport'] 62 | StarportCarrier: typeof import('vue-starport')['StarportCarrier'] 63 | Toc: typeof import('./node_modules/@slidev/client/builtin/Toc.vue')['default'] 64 | TocList: typeof import('./node_modules/@slidev/client/builtin/TocList.vue')['default'] 65 | Transform: typeof import('./node_modules/@slidev/client/builtin/Transform.vue')['default'] 66 | Tweet: typeof import('./node_modules/@slidev/client/builtin/Tweet.vue')['default'] 67 | VAfter: typeof import('./node_modules/@slidev/client/builtin/VAfter.ts')['default'] 68 | VClick: typeof import('./node_modules/@slidev/client/builtin/VClick.ts')['default'] 69 | VClicks: typeof import('./node_modules/@slidev/client/builtin/VClicks.ts')['default'] 70 | Youtube: typeof import('./node_modules/@slidev/client/builtin/Youtube.vue')['default'] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /postman/OWASP Top Ten Workshop.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "7354980f-b6fd-4039-a7ec-f8c288913a29", 4 | "name": "OWASP Top Ten Workshop", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "18638174" 7 | }, 8 | "item": [ 9 | { 10 | "name": "A01", 11 | "item": [ 12 | { 13 | "name": "A01: Access Control", 14 | "request": { 15 | "method": "GET", 16 | "header": [], 17 | "url": { 18 | "raw": "localhost:3000/profile?username=alice", 19 | "host": ["localhost"], 20 | "port": "3000", 21 | "path": ["profile"], 22 | "query": [ 23 | { 24 | "key": "username", 25 | "value": "alice" 26 | } 27 | ] 28 | } 29 | }, 30 | "response": [] 31 | } 32 | ] 33 | }, 34 | { 35 | "name": "A02", 36 | "item": [ 37 | { 38 | "name": "A02: All data exploit", 39 | "request": { 40 | "method": "GET", 41 | "header": [], 42 | "url": { 43 | "raw": "localhost:3000/all-data", 44 | "host": ["localhost"], 45 | "port": "3000", 46 | "path": ["all-data"] 47 | } 48 | }, 49 | "response": [] 50 | }, 51 | { 52 | "name": "A02: Change password", 53 | "request": { 54 | "method": "POST", 55 | "header": [], 56 | "body": { 57 | "mode": "raw", 58 | "raw": "{\n \"password\": \"newpassword\"\n}", 59 | "options": { 60 | "raw": { 61 | "language": "json" 62 | } 63 | } 64 | }, 65 | "url": { 66 | "raw": "localhost:3000/change-password", 67 | "host": ["localhost"], 68 | "port": "3000", 69 | "path": ["change-password"] 70 | } 71 | }, 72 | "response": [] 73 | }, 74 | { 75 | "name": "A02: Register", 76 | "request": { 77 | "method": "POST", 78 | "header": [], 79 | "body": { 80 | "mode": "raw", 81 | "raw": "{\n \"username\": \"newUser\",\n \"password\": \"password1234\"\n}", 82 | "options": { 83 | "raw": { 84 | "language": "json" 85 | } 86 | } 87 | }, 88 | "url": { 89 | "raw": "localhost:3000/register", 90 | "host": ["localhost"], 91 | "port": "3000", 92 | "path": ["register"] 93 | } 94 | }, 95 | "response": [] 96 | }, 97 | { 98 | "name": "A02: Login", 99 | "request": { 100 | "method": "POST", 101 | "header": [], 102 | "body": { 103 | "mode": "raw", 104 | "raw": "{\n \"username\": \"alice\",\n \"password\": \"482c811da5d5b4bc6d497ffa98491e38\"\n}", 105 | "options": { 106 | "raw": { 107 | "language": "json" 108 | } 109 | } 110 | }, 111 | "url": { 112 | "raw": "localhost:3000/login", 113 | "host": ["localhost"], 114 | "port": "3000", 115 | "path": ["login"] 116 | } 117 | }, 118 | "response": [] 119 | } 120 | ] 121 | }, 122 | { 123 | "name": "A03", 124 | "item": [ 125 | { 126 | "name": "A03: Get customer by name", 127 | "request": { 128 | "method": "GET", 129 | "header": [], 130 | "url": { 131 | "raw": "localhost:3000/customer?name=alice", 132 | "host": ["localhost"], 133 | "port": "3000", 134 | "path": ["customer"], 135 | "query": [ 136 | { 137 | "key": "name", 138 | "value": "alice" 139 | } 140 | ] 141 | } 142 | }, 143 | "response": [] 144 | }, 145 | { 146 | "name": "A03: SQL Injection", 147 | "request": { 148 | "method": "GET", 149 | "header": [], 150 | "url": { 151 | "raw": "localhost:3000/customer?name=' OR '1'='1", 152 | "host": ["localhost"], 153 | "port": "3000", 154 | "path": ["customer"], 155 | "query": [ 156 | { 157 | "key": "name", 158 | "value": "' OR '1'='1" 159 | } 160 | ] 161 | } 162 | }, 163 | "response": [] 164 | } 165 | ] 166 | }, 167 | { 168 | "name": "A04", 169 | "item": [ 170 | { 171 | "name": "A04: Buy a product", 172 | "request": { 173 | "method": "POST", 174 | "header": [], 175 | "url": { 176 | "raw": "localhost:3000/buy-product", 177 | "host": ["localhost"], 178 | "port": "3000", 179 | "path": ["buy-product"] 180 | } 181 | }, 182 | "response": [] 183 | } 184 | ] 185 | }, 186 | { 187 | "name": "A05", 188 | "item": [ 189 | { 190 | "name": "A05: Login", 191 | "request": { 192 | "method": "POST", 193 | "header": [], 194 | "body": { 195 | "mode": "raw", 196 | "raw": "{\n \"username\": \"alice\",\n \"password\": \"newpassword\"\n}", 197 | "options": { 198 | "raw": { 199 | "language": "json" 200 | } 201 | } 202 | }, 203 | "url": { 204 | "raw": "localhost:3000/login", 205 | "host": ["localhost"], 206 | "port": "3000", 207 | "path": ["login"] 208 | } 209 | }, 210 | "response": [] 211 | }, 212 | { 213 | "name": "A05: Profile", 214 | "request": { 215 | "method": "GET", 216 | "header": [], 217 | "url": { 218 | "raw": "localhost:3000/profile", 219 | "host": ["localhost"], 220 | "port": "3000", 221 | "path": ["profile"] 222 | } 223 | }, 224 | "response": [] 225 | } 226 | ] 227 | }, 228 | { 229 | "name": "A06", 230 | "item": [ 231 | { 232 | "name": "A06: Profile", 233 | "request": { 234 | "method": "GET", 235 | "header": [], 236 | "url": { 237 | "raw": "localhost:3000/profile?username=alice", 238 | "host": ["localhost"], 239 | "port": "3000", 240 | "path": ["profile"], 241 | "query": [ 242 | { 243 | "key": "username", 244 | "value": "alice" 245 | } 246 | ] 247 | } 248 | }, 249 | "response": [] 250 | }, 251 | { 252 | "name": "A06: Exploit vulnerability", 253 | "request": { 254 | "method": "GET", 255 | "header": [], 256 | "url": { 257 | "raw": "localhost:3000/profile?username=//127.0.0.1", 258 | "host": ["localhost"], 259 | "port": "3000", 260 | "path": ["profile"], 261 | "query": [ 262 | { 263 | "key": "username", 264 | "value": "//127.0.0.1" 265 | } 266 | ] 267 | } 268 | }, 269 | "response": [] 270 | } 271 | ] 272 | }, 273 | { 274 | "name": "A07", 275 | "item": [ 276 | { 277 | "name": "A07: Register", 278 | "request": { 279 | "method": "POST", 280 | "header": [], 281 | "body": { 282 | "mode": "raw", 283 | "raw": "{\n \"username\": \"bondJames\",\n \"password\": \"IamY0u\"\n}", 284 | "options": { 285 | "raw": { 286 | "language": "json" 287 | } 288 | } 289 | }, 290 | "url": { 291 | "raw": "localhost:3000/register", 292 | "host": ["localhost"], 293 | "port": "3000", 294 | "path": ["register"] 295 | } 296 | }, 297 | "response": [] 298 | } 299 | ] 300 | }, 301 | { 302 | "name": "A08", 303 | "item": [ 304 | { 305 | "name": "A08: Get profile from cookie", 306 | "request": { 307 | "method": "GET", 308 | "header": [ 309 | { 310 | "key": "Cookie", 311 | "value": "profile=eyJpZCI6MSwidXNlcm5hbWUiOiJfJCRORF9GVU5DJCRfZnVuY3Rpb24gKCkge1xuICAgIHRocm93IG5ldyBFcnJvcignc2VydmVyIGVycm9yJylcbiAgfSgpIn0=", 312 | "type": "text" 313 | } 314 | ], 315 | "url": { 316 | "raw": "localhost:3000/profile", 317 | "host": ["localhost"], 318 | "port": "3000", 319 | "path": ["profile"] 320 | } 321 | }, 322 | "response": [] 323 | } 324 | ] 325 | }, 326 | { 327 | "name": "A10", 328 | "item": [ 329 | { 330 | "name": "A10: Upload Image", 331 | "request": { 332 | "method": "POST", 333 | "header": [], 334 | "body": { 335 | "mode": "raw", 336 | "raw": "{\n \"imgUrl\": \"https://i.imgflip.com/6upp1a.jpg\"\n}", 337 | "options": { 338 | "raw": { 339 | "language": "json" 340 | } 341 | } 342 | }, 343 | "url": { 344 | "raw": "localhost:3000/user/image", 345 | "host": ["localhost"], 346 | "port": "3000", 347 | "path": ["user", "image"] 348 | } 349 | }, 350 | "response": [] 351 | }, 352 | { 353 | "name": "A10: Malicious Image url", 354 | "request": { 355 | "method": "POST", 356 | "header": [], 357 | "body": { 358 | "mode": "raw", 359 | "raw": "{\n \"imgUrl\": \"http://localhost:3001/secret\"\n}", 360 | "options": { 361 | "raw": { 362 | "language": "json" 363 | } 364 | } 365 | }, 366 | "url": { 367 | "raw": "localhost:3000/user/image", 368 | "host": ["localhost"], 369 | "port": "3000", 370 | "path": ["user", "image"] 371 | } 372 | }, 373 | "response": [] 374 | } 375 | ] 376 | } 377 | ], 378 | "auth": { 379 | "type": "bearer", 380 | "bearer": [ 381 | { 382 | "key": "token", 383 | "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhbGljZSIsImlhdCI6MTY2MjYzNzc2MH0.15w1NA_Kol5146DJEdXbDuIMmbVsiBXSGgzsVrV5NTY", 384 | "type": "string" 385 | } 386 | ] 387 | }, 388 | "event": [ 389 | { 390 | "listen": "prerequest", 391 | "script": { 392 | "type": "text/javascript", 393 | "exec": [""] 394 | } 395 | }, 396 | { 397 | "listen": "test", 398 | "script": { 399 | "type": "text/javascript", 400 | "exec": [""] 401 | } 402 | } 403 | ] 404 | } 405 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-ShareAlike 4.0 International Public 58 | License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-ShareAlike 4.0 International Public License ("Public 63 | License"). To the extent this Public License may be interpreted as a 64 | contract, You are granted the Licensed Rights in consideration of Your 65 | acceptance of these terms and conditions, and the Licensor grants You 66 | such rights in consideration of benefits the Licensor receives from 67 | making the Licensed Material available under these terms and 68 | conditions. 69 | 70 | Section 1 -- Definitions. 71 | 72 | a. Adapted Material means material subject to Copyright and Similar 73 | Rights that is derived from or based upon the Licensed Material 74 | and in which the Licensed Material is translated, altered, 75 | arranged, transformed, or otherwise modified in a manner requiring 76 | permission under the Copyright and Similar Rights held by the 77 | Licensor. For purposes of this Public License, where the Licensed 78 | Material is a musical work, performance, or sound recording, 79 | Adapted Material is always produced where the Licensed Material is 80 | synched in timed relation with a moving image. 81 | 82 | b. Adapter's License means the license You apply to Your Copyright 83 | and Similar Rights in Your contributions to Adapted Material in 84 | accordance with the terms and conditions of this Public License. 85 | 86 | c. BY-SA Compatible License means a license listed at 87 | creativecommons.org/compatiblelicenses, approved by Creative 88 | Commons as essentially the equivalent of this Public License. 89 | 90 | d. Copyright and Similar Rights means copyright and/or similar rights 91 | closely related to copyright including, without limitation, 92 | performance, broadcast, sound recording, and Sui Generis Database 93 | Rights, without regard to how the rights are labeled or 94 | categorized. For purposes of this Public License, the rights 95 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 96 | Rights. 97 | 98 | e. Effective Technological Measures means those measures that, in the 99 | absence of proper authority, may not be circumvented under laws 100 | fulfilling obligations under Article 11 of the WIPO Copyright 101 | Treaty adopted on December 20, 1996, and/or similar international 102 | agreements. 103 | 104 | f. Exceptions and Limitations means fair use, fair dealing, and/or 105 | any other exception or limitation to Copyright and Similar Rights 106 | that applies to Your use of the Licensed Material. 107 | 108 | g. License Elements means the license attributes listed in the name 109 | of a Creative Commons Public License. The License Elements of this 110 | Public License are Attribution and ShareAlike. 111 | 112 | h. Licensed Material means the artistic or literary work, database, 113 | or other material to which the Licensor applied this Public 114 | License. 115 | 116 | i. Licensed Rights means the rights granted to You subject to the 117 | terms and conditions of this Public License, which are limited to 118 | all Copyright and Similar Rights that apply to Your use of the 119 | Licensed Material and that the Licensor has authority to license. 120 | 121 | j. Licensor means the individual(s) or entity(ies) granting rights 122 | under this Public License. 123 | 124 | k. Share means to provide material to the public by any means or 125 | process that requires permission under the Licensed Rights, such 126 | as reproduction, public display, public performance, distribution, 127 | dissemination, communication, or importation, and to make material 128 | available to the public including in ways that members of the 129 | public may access the material from a place and at a time 130 | individually chosen by them. 131 | 132 | l. Sui Generis Database Rights means rights other than copyright 133 | resulting from Directive 96/9/EC of the European Parliament and of 134 | the Council of 11 March 1996 on the legal protection of databases, 135 | as amended and/or succeeded, as well as other essentially 136 | equivalent rights anywhere in the world. 137 | 138 | m. You means the individual or entity exercising the Licensed Rights 139 | under this Public License. Your has a corresponding meaning. 140 | 141 | Section 2 -- Scope. 142 | 143 | a. License grant. 144 | 145 | 1. Subject to the terms and conditions of this Public License, 146 | the Licensor hereby grants You a worldwide, royalty-free, 147 | non-sublicensable, non-exclusive, irrevocable license to 148 | exercise the Licensed Rights in the Licensed Material to: 149 | 150 | a. reproduce and Share the Licensed Material, in whole or 151 | in part; and 152 | 153 | b. produce, reproduce, and Share Adapted Material. 154 | 155 | 2. Exceptions and Limitations. For the avoidance of doubt, where 156 | Exceptions and Limitations apply to Your use, this Public 157 | License does not apply, and You do not need to comply with 158 | its terms and conditions. 159 | 160 | 3. Term. The term of this Public License is specified in Section 161 | 6(a). 162 | 163 | 4. Media and formats; technical modifications allowed. The 164 | Licensor authorizes You to exercise the Licensed Rights in 165 | all media and formats whether now known or hereafter created, 166 | and to make technical modifications necessary to do so. The 167 | Licensor waives and/or agrees not to assert any right or 168 | authority to forbid You from making technical modifications 169 | necessary to exercise the Licensed Rights, including 170 | technical modifications necessary to circumvent Effective 171 | Technological Measures. For purposes of this Public License, 172 | simply making modifications authorized by this Section 2(a) 173 | (4) never produces Adapted Material. 174 | 175 | 5. Downstream recipients. 176 | 177 | a. Offer from the Licensor -- Licensed Material. Every 178 | recipient of the Licensed Material automatically 179 | receives an offer from the Licensor to exercise the 180 | Licensed Rights under the terms and conditions of this 181 | Public License. 182 | 183 | b. Additional offer from the Licensor -- Adapted Material. 184 | Every recipient of Adapted Material from You 185 | automatically receives an offer from the Licensor to 186 | exercise the Licensed Rights in the Adapted Material 187 | under the conditions of the Adapter's License You apply. 188 | 189 | c. No downstream restrictions. You may not offer or impose 190 | any additional or different terms or conditions on, or 191 | apply any Effective Technological Measures to, the 192 | Licensed Material if doing so restricts exercise of the 193 | Licensed Rights by any recipient of the Licensed 194 | Material. 195 | 196 | 6. No endorsement. Nothing in this Public License constitutes or 197 | may be construed as permission to assert or imply that You 198 | are, or that Your use of the Licensed Material is, connected 199 | with, or sponsored, endorsed, or granted official status by, 200 | the Licensor or others designated to receive attribution as 201 | provided in Section 3(a)(1)(A)(i). 202 | 203 | b. Other rights. 204 | 205 | 1. Moral rights, such as the right of integrity, are not 206 | licensed under this Public License, nor are publicity, 207 | privacy, and/or other similar personality rights; however, to 208 | the extent possible, the Licensor waives and/or agrees not to 209 | assert any such rights held by the Licensor to the limited 210 | extent necessary to allow You to exercise the Licensed 211 | Rights, but not otherwise. 212 | 213 | 2. Patent and trademark rights are not licensed under this 214 | Public License. 215 | 216 | 3. To the extent possible, the Licensor waives any right to 217 | collect royalties from You for the exercise of the Licensed 218 | Rights, whether directly or through a collecting society 219 | under any voluntary or waivable statutory or compulsory 220 | licensing scheme. In all other cases the Licensor expressly 221 | reserves any right to collect such royalties. 222 | 223 | Section 3 -- License Conditions. 224 | 225 | Your exercise of the Licensed Rights is expressly made subject to the 226 | following conditions. 227 | 228 | a. Attribution. 229 | 230 | 1. If You Share the Licensed Material (including in modified 231 | form), You must: 232 | 233 | a. retain the following if it is supplied by the Licensor 234 | with the Licensed Material: 235 | 236 | i. identification of the creator(s) of the Licensed 237 | Material and any others designated to receive 238 | attribution, in any reasonable manner requested by 239 | the Licensor (including by pseudonym if 240 | designated); 241 | 242 | ii. a copyright notice; 243 | 244 | iii. a notice that refers to this Public License; 245 | 246 | iv. a notice that refers to the disclaimer of 247 | warranties; 248 | 249 | v. a URI or hyperlink to the Licensed Material to the 250 | extent reasonably practicable; 251 | 252 | b. indicate if You modified the Licensed Material and 253 | retain an indication of any previous modifications; and 254 | 255 | c. indicate the Licensed Material is licensed under this 256 | Public License, and include the text of, or the URI or 257 | hyperlink to, this Public License. 258 | 259 | 2. You may satisfy the conditions in Section 3(a)(1) in any 260 | reasonable manner based on the medium, means, and context in 261 | which You Share the Licensed Material. For example, it may be 262 | reasonable to satisfy the conditions by providing a URI or 263 | hyperlink to a resource that includes the required 264 | information. 265 | 266 | 3. If requested by the Licensor, You must remove any of the 267 | information required by Section 3(a)(1)(A) to the extent 268 | reasonably practicable. 269 | 270 | b. ShareAlike. 271 | 272 | In addition to the conditions in Section 3(a), if You Share 273 | Adapted Material You produce, the following conditions also apply. 274 | 275 | 1. The Adapter's License You apply must be a Creative Commons 276 | license with the same License Elements, this version or 277 | later, or a BY-SA Compatible License. 278 | 279 | 2. You must include the text of, or the URI or hyperlink to, the 280 | Adapter's License You apply. You may satisfy this condition 281 | in any reasonable manner based on the medium, means, and 282 | context in which You Share Adapted Material. 283 | 284 | 3. You may not offer or impose any additional or different terms 285 | or conditions on, or apply any Effective Technological 286 | Measures to, Adapted Material that restrict exercise of the 287 | rights granted under the Adapter's License You apply. 288 | 289 | Section 4 -- Sui Generis Database Rights. 290 | 291 | Where the Licensed Rights include Sui Generis Database Rights that 292 | apply to Your use of the Licensed Material: 293 | 294 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 295 | to extract, reuse, reproduce, and Share all or a substantial 296 | portion of the contents of the database; 297 | 298 | b. if You include all or a substantial portion of the database 299 | contents in a database in which You have Sui Generis Database 300 | Rights, then the database in which You have Sui Generis Database 301 | Rights (but not its individual contents) is Adapted Material, 302 | 303 | including for purposes of Section 3(b); and 304 | 305 | c. You must comply with the conditions in Section 3(a) if You Share 306 | all or a substantial portion of the contents of the database. 307 | 308 | For the avoidance of doubt, this Section 4 supplements and does not 309 | replace Your obligations under this Public License where the Licensed 310 | Rights include other Copyright and Similar Rights. 311 | 312 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 313 | 314 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 315 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 316 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 317 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 318 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 319 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 320 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 321 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 322 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 323 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 324 | 325 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 326 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 327 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 328 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 329 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 330 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 331 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 332 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 333 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 334 | 335 | c. The disclaimer of warranties and limitation of liability provided 336 | above shall be interpreted in a manner that, to the extent 337 | possible, most closely approximates an absolute disclaimer and 338 | waiver of all liability. 339 | 340 | Section 6 -- Term and Termination. 341 | 342 | a. This Public License applies for the term of the Copyright and 343 | Similar Rights licensed here. However, if You fail to comply with 344 | this Public License, then Your rights under this Public License 345 | terminate automatically. 346 | 347 | b. Where Your right to use the Licensed Material has terminated under 348 | Section 6(a), it reinstates: 349 | 350 | 1. automatically as of the date the violation is cured, provided 351 | it is cured within 30 days of Your discovery of the 352 | violation; or 353 | 354 | 2. upon express reinstatement by the Licensor. 355 | 356 | For the avoidance of doubt, this Section 6(b) does not affect any 357 | right the Licensor may have to seek remedies for Your violations 358 | of this Public License. 359 | 360 | c. For the avoidance of doubt, the Licensor may also offer the 361 | Licensed Material under separate terms or conditions or stop 362 | distributing the Licensed Material at any time; however, doing so 363 | will not terminate this Public License. 364 | 365 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 366 | License. 367 | 368 | Section 7 -- Other Terms and Conditions. 369 | 370 | a. The Licensor shall not be bound by any additional or different 371 | terms or conditions communicated by You unless expressly agreed. 372 | 373 | b. Any arrangements, understandings, or agreements regarding the 374 | Licensed Material not stated herein are separate from and 375 | independent of the terms and conditions of this Public License. 376 | 377 | Section 8 -- Interpretation. 378 | 379 | a. For the avoidance of doubt, this Public License does not, and 380 | shall not be interpreted to, reduce, limit, restrict, or impose 381 | conditions on any use of the Licensed Material that could lawfully 382 | be made without permission under this Public License. 383 | 384 | b. To the extent possible, if any provision of this Public License is 385 | deemed unenforceable, it shall be automatically reformed to the 386 | minimum extent necessary to make it enforceable. If the provision 387 | cannot be reformed, it shall be severed from this Public License 388 | without affecting the enforceability of the remaining terms and 389 | conditions. 390 | 391 | c. No term or condition of this Public License will be waived and no 392 | failure to comply consented to unless expressly agreed to by the 393 | Licensor. 394 | 395 | d. Nothing in this Public License constitutes or may be interpreted 396 | as a limitation upon, or waiver of, any privileges and immunities 397 | that apply to the Licensor or You, including from the legal 398 | processes of any jurisdiction or authority. 399 | 400 | ======================================================================= 401 | 402 | Creative Commons is not a party to its public licenses. 403 | Notwithstanding, Creative Commons may elect to apply one of its public 404 | licenses to material it publishes and in those instances will be 405 | considered the “Licensor.” The text of the Creative Commons public 406 | licenses is dedicated to the public domain under the CC0 Public Domain 407 | Dedication. Except for the limited purpose of indicating that material 408 | is shared under a Creative Commons public license or as otherwise 409 | permitted by the Creative Commons policies published at 410 | creativecommons.org/policies, Creative Commons does not authorize the 411 | use of the trademark "Creative Commons" or any other trademark or logo 412 | of Creative Commons without its prior written consent including, 413 | without limitation, in connection with any unauthorized modifications 414 | to any of its public licenses or any other arrangements, 415 | understandings, or agreements concerning use of licensed material. For 416 | the avoidance of doubt, this paragraph does not form part of the public 417 | licenses. 418 | 419 | Creative Commons may be contacted at creativecommons.org. 420 | -------------------------------------------------------------------------------- /slides.md: -------------------------------------------------------------------------------- 1 | --- 2 | theme: slidev-theme-nearform 3 | layout: default 4 | highlighter: shiki 5 | lineNumbers: false 6 | --- 7 | 8 | 9 | 10 | # OWASP Top Ten Security Vulnerabilities Workshop 11 | 12 | 13 | 14 | 19 | 20 | --- 21 | 22 | # Introduction: OWASP 🐝 23 | 24 |
25 | 26 | - The Open Web Application Security Project (**OWASP**) is a **non profit** foundation that works to improve the security of software 27 | 28 | - OWASP has community-led **open-source** software projects, hundreds of local chapters worldwide, tens of thousands of members, and leading educational and training conferences 29 | 30 |
31 | 32 | --- 33 | 34 | # Introduction: OWASP Top Ten 35 | 36 |
37 | 38 | - The **[OWASP Top Ten](https://owasp.org/Top10/)** is a standard awareness document for developers and web application security 39 | - It represents a broad consensus about the most critical security risks to web applications 40 | 41 |
42 | 43 | --- 44 | 45 | # Workshop Setup 46 | 47 |
48 | 49 | - This workshop will explain each of the 10 vulnerabilities 50 | - There is a Fastify node.js server demonstrating the security issues 51 | - At each step you are asked to fix the vulnerability in the server 52 | - You will find the solution to each step in the file `solution.js` inside `src/a{n}-{name}` folder (the actual path may vary) 53 | - The 💡 icon indicates hints 54 | 55 |
56 | 57 | --- 58 | 59 | # Getting setup 60 | 61 |
62 | 63 | #### Requirements 64 | 65 |
66 | 67 | - Node LTS 68 | - docker 69 | - docker-compose 70 | - Postman (if you want to be able to test vulnerabilities) 71 | 72 | #### Setup 73 | 74 |
75 | 76 | ```bash 77 | git clone https://github.com/nearform/owasp-top-ten-workshop 78 | npm ci 79 | npm run db:up 80 | npm run db:migrate 81 | ``` 82 | 83 |
84 | 85 | --- 86 | 87 | # Running the modules 88 | 89 |
90 | 91 | - `cd src/a{x}-step-name` 92 | - `npm start` will run the server ready to respond to requests 93 | - `npm run verify` will run automated tests that fail by default until this step's issue is solved 94 | - Check out README.md in the projects for potential additional info 95 | 96 |
97 | 98 | ### Example 99 | 100 | ```bash 101 | cd src/a01-access-control 102 | npm start 103 | ``` 104 | 105 | The server for that step will run on http://localhost:3000 106 | 107 | --- 108 | 109 | # Testing the vulnerabilities 110 | 111 |
112 | 113 | - Some vulnerabilities involve sending specific requests to the server. We will be using the tool Postman to send those requests 114 | - The `postman` folder contains a collection that can be imported into Postman to easily send those requests 115 | - The Postman collection is pre-logged in with user `alice`. The `Bearer token` is set that represents `alice`. 116 | 117 |
118 | 119 | --- 120 | 121 | # Automated testing 122 | 123 |
124 | 125 | - Some vulnerabilities have automated tests which verify the presence of the vulnerability 126 | - Those tests will fail until you fix the vulnerability 127 | - `npm run verify` in a step's folder 128 | 129 |
130 | 131 | --- 132 | 133 | # A01 Broken Access Control 134 | 135 |
136 | 137 | - [AO1:2021 - Broken Access Control](https://owasp.org/Top10/A01_2021-Broken_Access_Control/) 138 | - User should only act based on specific permissions 139 | - Incorrect permissions -> unauthorised access of data 140 |
141 | 142 | --- 143 | 144 | # A01 Common vulnerabilities 145 | 146 |
147 | 148 | - Not applying principle of **least privilege** 149 | - Checks can be bypassed by tampering with page or request 150 | - Allowing access to someone else's info by knowing the UUID 151 | - CORS allows untrusted origins 152 | 153 |
154 | 155 | --- 156 | 157 | # A01 How to Prevent 158 | 159 |
160 | 161 | - Deny access by default 162 | - Avoid duplication of access control logic 163 | - Enforce **user ownership** when manipulating data 164 | 165 |
166 | 167 | --- 168 | 169 | # A01 Exercise 170 | 171 |
172 | 173 | - The `/profile` route returns sensitive user data 174 | - It takes a `username` query parameter to return the user's info 175 | 176 | ``` 177 | GET http://localhost:3000/profile?username=alice 178 | ``` 179 | 180 | ```json 181 | { 182 | "id": 1, 183 | "username": "alice", 184 | "age": 23 185 | } 186 | ``` 187 | 188 |
189 | 190 | --- 191 | 192 | # A01 The problem 193 | 194 |
195 | 196 | - Run the server for step 1 (`cd src/a01-access-control`, `npm start`) 197 | - In Postman, run the query for A01: Access Control. Observe the data for Alice being returned 198 | - Now change the `username` query parameter to `bob`. Result: 199 | 200 | ```json 201 | { 202 | "id": 2, 203 | "username": "bob", 204 | "age": 31 205 | } 206 | ``` 207 | 208 | - Bob's data is being exposed! Remember we're logged in as Alice and shouldn't see Bob's data 209 | 210 |
211 | 212 | --- 213 | 214 | # A01 Fixing it 🪄 215 | 216 |
217 | 218 | - Run the automated tests for step 1 - `npm run verify` 219 | - The tests fail because the server shouldn't return Bob's data 220 | - Edit the `/profile` route in the exercise folder to return only the `logged-in` user's profile without exposing other people's profiles 221 | - 💡 The server uses [fastify-jwt](https://github.com/fastify/fastify-jwt) to handle authentication 222 | 223 |
224 | 225 | --- 226 | 227 | # A01 Solution 💡 228 | 229 |
230 | 231 | - The issue comes from the usage of a user-supplied `query` parameter to choose which profile's info to check 232 | - The server should instead fetch the only the `logged-in` user's info and reply with `403` in other cases 233 | 234 | ```js 235 | async req => { 236 | if (!req.user) { 237 | throw new errors.Unauthorized() 238 | } 239 | const username = req.user.username // 💡 We get the username from the logged in user, not from the query! 240 | if (username !== req.query.username) { 241 | throw new errors.Forbidden() // if does not match with the user's one, return a 403 Forbidden error 242 | } 243 | return user 244 | } 245 | ``` 246 | 247 |
248 | 249 | --- 250 | 251 | # A02 Cryptographic Failures 252 | 253 |
254 | 255 | - [A02: Cryptographic Failures](https://owasp.org/Top10/A02_2021-Cryptographic_Failures/) 256 | - Weak or inexistent **cryptography** of sensitive data 257 | - Passwords, credit card numbers, health records, personal information, business secrets... 258 | - Anything protected by privacy laws or other regulations 259 | 260 |
261 | 262 | --- 263 | 264 | # A02 Common vulnerabilities 265 | 266 |
267 | 268 | - Weak or outdated cryptographic algorithms like **md5 ⚠️** 269 | - Weak secret keys, default ones, keys from online tutorials, keys checked in source control... 270 | - Lack of traffic encryption (**HTTPS**) 271 | - Insufficient entropy in seed generation 272 | 273 |
274 | 275 | --- 276 | 277 | # A02 How to Prevent 278 | 279 |
280 | 281 | - Check sensitive data is well **encrypted**. Avoid storing sensitive data unnecessarily 282 | - Use up to date and strong standard algorithms 283 | - Proper key/secrets management (**private keys in git ⚠️**) 284 | - Disable caching for responses that contain sensitive data 285 | 286 |
287 | 288 | --- 289 | 290 | # A02 A weak hashing algorithm 291 | 292 |
293 | 294 | - The `/all-data` endpoint returns all users and their passwords (hashed using md5). Imagine this as a data breach 295 | - Result of the `all-data` endpoint 296 | 297 | ```json 298 | [ 299 | { 300 | "username": "bob", 301 | "password": "884a22eb30e5cfd71894d43ac553faa5" 302 | }, 303 | { 304 | "username": "alice", 305 | "password": "5e9d11a14ad1c8dd77e98ef9b53fd1ba" 306 | } 307 | ] 308 | ``` 309 | 310 |
311 | 312 | --- 313 | 314 | # A02 The problem 315 | 316 |
317 | 318 | - Using the `/all-data` route, find Alice's hashed password 319 | - With this password hash, try to find the original password Alice created 320 | - the Postman collection contains requests for doing the queries 321 | - 💡 There are websites to decrypt md5 322 | 323 |
324 | 325 | --- 326 | 327 | # A02 Fixing it 🪄 328 | 329 |
330 | 331 | - md5 hash is vulnerable and shouldn't be used 332 | - In `src/a02-cryptographic-failure`, fix the hashing algorithm used to be a strong algorithm instead of md5 333 | - Using [bcrypt](https://www.npmjs.com/package/bcrypt) is a good idea for passwords 334 | - The application exposes a `/change-password` route used to change a user's password 335 | 336 |
337 | 338 | --- 339 | 340 | # A02 Solution 💡 341 | 342 |
343 | 344 | ```js 345 | // utils/crypto.js 346 | import { hash, compare } from 'bcrypt' 347 | 348 | const saltRounds = 10 349 | 350 | export async function hashPassword(password) { 351 | return await hash(password, saltRounds) 352 | } 353 | ``` 354 | 355 |
356 | 357 | --- 358 | 359 | # A02 Solution 💡 (2) 360 | 361 |
362 | 363 | ```js 364 | // utils/crypto.js 365 | export async function comparePassword(password, hash) { 366 | return await compare(password, hash) 367 | } 368 | ``` 369 | 370 |
371 | 372 | --- 373 | 374 | # A03 Injection 💉 375 | 376 |
377 | 378 | - [A03: Injection](https://owasp.org/Top10/A03_2021-Injection/) 379 | - 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 380 | - Injections can result in data loss or corruption, lack of accountability, or denial of access. Injections can sometimes lead to complete host takeover. 381 | - Common targets: **SQL, NoSQL, ORM, LDAP, JS eval** 382 | 383 |
384 | 385 | --- 386 | 387 | # A03 Common vulnerabilities 388 | 389 |
390 | 391 | - Lack of validation or **sanitization** of user input 392 | - Dynamic queries or non-parameterized calls without context-aware **escaping** are used directly in the interpreter 393 | - Hostile data is used within object-relational mapping (ORM) **search parameters** to extract additional, sensitive records 394 | 395 |
396 | 397 | --- 398 | 399 | # A03 Attack 400 | 401 |
402 | 403 | - Run the server for step 3 (`cd src/a03-injection`, `npm start`) 404 | - In Postman, run the query for `A03: Get customer by name`. Observe the data for `name: "alice"` being returned 405 | - Try to run the query for `A03: SQL Injection`. Observe all the customers being returned 406 | - The query param value `' OR '1'='1` takes advantage of the unsafe string concatenation to create this SQL query 407 | `SELECT * FROM customers WHERE name='' OR '1'='1'` which will return every record in the table 408 | 409 |
410 | 411 | --- 412 | 413 | # A03 Fixing it 🪄 414 | 415 |
416 | 417 | - 💡 Prefer using a safe API that **sanitizes input** e.g `@nearform/sql` 418 | - **Escape special characters** using the specific escape syntax for that interpreter 419 | - Avoid user-supplied table names or column names as they cannot be escaped 420 | - **Automated testing** of all parameters, headers, URL, cookies, JSON, SOAP, and XML data inputs is strongly encouraged 421 | 422 |
423 | 424 | --- 425 | 426 | # A03 Solution 💡 427 | 428 |
429 | 430 | - The `@nearform/sql` library escapes special characters contained in the user's input 431 | 432 | ```js 433 | // import SQL from '@nearform/sql' 434 | export default async function customer(fastify) { 435 | fastify.get( 436 | '/customer', 437 | { 438 | onRequest: [fastify.authenticate] 439 | }, 440 | async req => { 441 | const { name } = req.query 442 | const { rows: customers } = await fastify.pg.query( 443 | SQL`SELECT * FROM customers WHERE name=${name}` 444 | ) 445 | if (!customers.length) throw errors.NotFound() 446 | return customers 447 | } 448 | ) 449 | } 450 | ``` 451 | 452 |
453 | 454 | --- 455 | 456 | # A04 Insecure Design 457 | 458 |
459 | 460 | - [A04: Insecure Design](https://owasp.org/Top10/A04_2021-Insecure_Design/) 461 | - Fundamental **design flaws** of the software can cause security issues 462 | - Those issues cannot be fixed by a better more secure code implementation 463 | - Failure to determine the level of security required during design 464 | 465 |
466 | 467 | --- 468 | 469 | # A04 Examples 470 | 471 |
472 | 473 | - A forgotten password flow with **_"security questions"_** is insecure by design because more than one person can know the answer 474 | - An ecommerce website sells high-end video cards that scalpers buy with bots to resell (bad PR with customers) 475 | - 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 476 | 477 |
478 | 479 | --- 480 | 481 | # A04 How to prevent 482 | 483 |
484 | 485 | - Model threats for the application, all its flows and business logic 486 | - Continuously evaluate security requirement and design during the development lifecycle 487 | - Consider security rules and access controls for every user story 488 | - **Use unit and integration tests** to verify the application is resistant to the threat model 489 | 490 |
491 | 492 | --- 493 | 494 | # A04 Exercise 495 | 496 |
497 | 498 | - The `/buy-product` endpoint does not have protection against bots run by scalpers 499 | - It means that someone can buy a lot of stock quickly and leave legitimate customers without any 500 | 501 |
502 | 503 | --- 504 | 505 | # A04 Scalping 506 | 507 |
508 | 509 | - Run the server for step 4 (`cd src/a04-insecure-design`, `npm start`) 510 | - In Postman, run the query for `A04: Buy product`. Observe the data for `success: true` being returned 511 | - Run the query many times in a row in a short period of time 512 | - Notice that there is no protection against multiple sequential purchases 513 |
514 | 515 | --- 516 | 517 | # A04 Fixing it 🪄 518 | 519 |
520 | 521 | - Prefer using rate limiter for your routes 522 | - 💡 Using [`fastify-rate-limit`](https://github.com/fastify/fastify-rate-limit) is a good idea for setting a rate limit 523 | - Let's consider a scenario where a user can buy a maximum of two products per minute 524 | - Edit the `/buy-product` route in the exercise folder considering the scenario above 525 | 526 |
527 | 528 | --- 529 | 530 | # A04 Solution 💡 531 | 532 |
533 | 534 | ```js 535 | // register the rateLimit plugin in the server.js file 536 | await fastify.register(rateLimit) 537 | ``` 538 | 539 | ```js 540 | // the rate limit is set to a maximum of two purchases per minute 541 | export default async function ecommerce(fastify) { 542 | fastify.post( 543 | '/buy-product', 544 | { 545 | config: { 546 | rateLimit: { 547 | max: 2, 548 | timeWindow: '1 minute' 549 | } 550 | } 551 | }, 552 | (req, reply) => { 553 | reply.send({ success: true }) 554 | } 555 | ) 556 | } 557 | ``` 558 | 559 |
560 | 561 | --- 562 | 563 | # A05 Security Misconfiguration 564 | 565 |
566 | 567 | - Security misconfigurations are security controls that are inaccurately configured or left insecure, putting your systems and data at risk 568 | - **Badly configured** servers or services can lead to vulnerabilities 569 | - With increased usage of highly configurable software and cloud APIs, there are many opportunities for misconfiguration 570 | 571 |
572 | 573 | --- 574 | 575 | # A05 Common Vulnerabilities 576 | 577 |
578 | 579 | - Improperly configured **permissions** or security settings 580 | - Unnecessary features enabled: open ports, services, accounts with elevated accesss... 581 | - **Default credentials** unchanged 582 | - Out of date or vulnerable server 583 | - Stack trace from error handling revealing information to users 584 | 585 |
586 | 587 | --- 588 | 589 | # A05 How to prevent 590 | 591 |
592 | 593 | - Repeatable, automated and fast to deploy environments 594 | - Different credentials should be used in each environment 595 | - Frequently review security updates, patches and permissions 596 | - A **segmented architecture** increases security by separating components, tenants, containers or cloud security groups 597 | - An automated **test** to verify the effectiveness of the configurations 598 | 599 |
600 | 601 | --- 602 | 603 | # A05 Unsigned Cookies 🍪 604 | 605 |
606 | 607 | - Run the server for step 5 (`cd src/a05-security-misconfiguration`, `npm start`) 608 | - In Postman, run the query for `A05: Login`. Observe a cookie with `userId=1` being returned 609 | - Try to run the query for `A05: Profile`. Observe the information about profile with `userId=1` being returned 610 | - Try to [change the value of the cookie](https://learning.postman.com/docs/sending-requests/cookies/) to `userId=2`. Observe information about `userId=2` being returned 611 | 612 |
613 | 614 | --- 615 | 616 | # A05 Fixing it 🪄 617 | 618 |
619 | 620 | - 💡 Cookie must always be **[signed](https://github.com/fastify/fastify-cookie)** to ensure they are not getting tampered with on client-side by an attacker 621 | - It's important to use **httpOnly** cookies to prevent the cookie being accessed through client side script 622 | - Store the **signing secret** safely. 623 | - Don't store sentive information in cleartext 624 | 625 |
626 | 627 | --- 628 | 629 | # A05 Solution 💡 630 | 631 |
632 | 633 | ```js 634 | export function login(fastify) { 635 | fastify.post('/login', { schema }, async (req, rep) => { 636 | const { username, password } = req.body 637 | const { 638 | rows: [user] 639 | } = await fastify.pg.query( 640 | SQL`SELECT id, username, password FROM users WHERE username = ${username}` 641 | ) 642 | if (!user) { 643 | throw errors.Unauthorized('No matching user found') 644 | } 645 | const passwordMatch = await comparePassword(password, user.password) 646 | if (!passwordMatch) { 647 | throw errors.Unauthorized('Invalid Password') 648 | } 649 | rep.setCookie('userId', JSON.stringify(user.id), { 650 | signed: true, // 💡 signing the cookie 651 | httpOnly: true // http only 652 | }) 653 | return 'user logged in' 654 | }) 655 | } 656 | ``` 657 | 658 |
659 | 660 | --- 661 | 662 | # A05 Solution 💡 (2) 663 | 664 |
665 | 666 | ```js 667 | export function profile(fastify) { 668 | fastify.get('/profile', async req => { 669 | const { value: id, valid } = fastify.unsignCookie( 670 | //unsign the cookie and check validity 671 | req?.cookies?.userId || '' 672 | ) 673 | 674 | if (!valid) { 675 | // check if the cookie has been tampered 676 | throw new errors.Unauthorized() 677 | } 678 | const { 679 | rows: [user] 680 | } = await fastify.pg.query( 681 | SQL`SELECT id, username, age FROM users WHERE id = ${id}` 682 | ) 683 | if (!user) { 684 | throw new errors.NotFound() 685 | } 686 | return user 687 | }) 688 | } 689 | ``` 690 | 691 |
692 | 693 | --- 694 | 695 | # A06 Vulnerable and Outdated Components 696 | 697 |
698 | 699 | - Applications use a variety of components and libraries which can have security issues 700 | - Vulnerable components can be an attack vector until they are patched 701 | - Particularly relevant in the node.js world with an ever-growing **NPM dependency tree** 702 | 703 |
704 | 705 | --- 706 | 707 | # A06 Common Vulnerabilities 708 | 709 |
710 | 711 | - Not **tracking versions** of used components, including nested dependencies 712 | - Using unsupported or vulnerable software, including OS, web server, database, APIs, libraries, components, runtimes... 713 | - Not **scanning for vulnerabilities** regularly and subscribing to security news for used components 714 | - Not fixing vulnerable dependencies in a timely fashion 715 | 716 |
717 | 718 | --- 719 | 720 | # A06 How to prevent 721 | 722 |
723 | 724 | - Remove **unused dependencies**, unnecessary features, components, files, and documentation 725 | - Continuously inventory the versions of components and their dependencies 726 | - Monitor for libraries and components that are **unmaintained** or do not create security patches for older versions 727 | - Only obtain components from **official** sources over secure links 728 | 729 |
730 | 731 | --- 732 | 733 | # A06 The attack 🥷 734 | 735 |
736 | 737 | - Run the server for step 6 (`cd src/a06-vulnerable-outdated`, `npm start`) 738 | - In Postman, run the query for `A06: Profile`. Observe error `404` being returned 739 | - Try to run the query for `A06: Exploit vulnerability`. Observe the error message response 740 |
741 | 742 | --- 743 | 744 | # A06 The attack (2) 745 | 746 |
747 | 748 | - Because of an outdated version of the HTTP client library **[undici](https://github.com/nodejs/undici)** we can exploit a known **[vulnerability](https://www.cvedetails.com/cve/CVE-2022-35949/)** 749 | - 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 to `127.0.0.1:80` 750 |
751 | 752 | --- 753 | 754 | # A06 Fixing it 🪄 755 | 756 |
757 | 758 | - 💡 Update the library to a version in which this vulnerability was fixed 759 | 760 |
761 | 762 | --- 763 | 764 | # A06 The Solution 💡 765 | 766 |
767 | 768 | ```js 769 | import { request } from 'undici' //💡 updated undici version >= 5.8.1 770 | export default function (fastify) { 771 | fastify.get( 772 | '/profile', 773 | { 774 | onRequest: [fastify.authenticate] 775 | }, 776 | async req => { 777 | const { username } = req.query 778 | if (/^\//.test(username)) { 779 | // check username doesn't start with / 780 | throw errors.BadRequest() 781 | } 782 | const { body, statusCode } = await request({ 783 | origin: 'http://example.com', 784 | pathname: username 785 | }) 786 | if (statusCode !== 200) { 787 | throw errors.NotFound() 788 | } 789 | return body 790 | } 791 | ) 792 | } 793 | ``` 794 | 795 |
796 | 797 | --- 798 | 799 | # A07 Identification and Authentication Failures 800 | 801 |
802 | 803 | - Verification of the user's identity, **authentication**, and session management is crucial to security 804 | - Weak or vulnerable authentication systems can be exploited to gain access 805 | - Systems with broken authentication can lead to data breaches and passwords leak 806 | 807 |
808 | 809 | --- 810 | 811 | # A07 Common Vulnerabilities 812 | 813 |
814 | 815 | - Application allows for credentials stuffing or brute forcing 816 | - Allows default, weak or known passwords 817 | - Exploitable credential recovery processes 818 | - Lack of effective **multi-factor authentication** 819 | - Unencrypted or weakly encrypted **password storage** 820 | 821 |
822 | 823 | --- 824 | 825 | # A07 How to prevent 826 | 827 |
828 | 829 | - Where possible, implement **multi-factor authentication** 830 | - Require **strong passwords** (length, complexity, rotation policies, and don't allow leaked passwords use) 831 | - Ensure registration and credential recovery use the same messages for all outcomes 832 | - Limit or increasingly delay failed login attempts 833 | - Use secure password data store practices (salting + hashing) 834 | 835 |
836 | 837 | --- 838 | 839 | # A07 Allowing leaked passwords 💧 840 | 841 |
842 | 843 | - In 2017 the NIST **[recommended](https://pages.nist.gov/800-63-3/sp800-63b.html#sec4:~:text=when%20processing%20requests,a%20different%20value)** that websites should check all new passwords against available lists of data breaches. 844 | - This practice has been adopted by OWASP and became part of their **[recommendation]()**. 845 | - In the real scenario you should try to use something like **[Have I Been Pwned](https://haveibeenpwned.com/Passwords)** 846 | 847 |
848 | 849 | --- 850 | 851 | # A07 Allowing leaked passwords 💧 (2) 852 | 853 |
854 | 855 | - In the workshop - the database contains the list of leaked passwords in `databreachrecords` 856 | - Run the server for step 7 (`cd src/a07-authentication-failures`, `npm start`) 857 | - In Postman, run the query for `A07: Register`. Observe a token is succesfully returned 858 | - In the database check the `databreachrecords` table for password used in the Postman request body 859 | 860 |
861 | 862 | --- 863 | 864 | # A07 Allowing leaked passwords 💧 (3) 865 | 866 |
867 | 868 | - It should not allow to use passwords that are known to be leaked 869 | - Instead it should require the user to set a different password indicating what is the reason 870 | - Place your solution in the `routes/user/index.js` 871 | - Test it by running `npm run verify` (it will fail initially) 872 | 873 |
874 | 875 | --- 876 | 877 | # A07 Fixing it 🪄 878 | 879 |
880 | 881 | - 💡 Using data available in `dataBreachRecords` check if requested password is safe to use 882 | - Run `sql` query inside the `/register` endpoint to check if the password is there 883 | - Return a `400` error with `message` 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.'` 884 | 885 |
886 | 887 | --- 888 | 889 | # A07 Solution 💡 890 | 891 |
892 | 893 | ```js 894 | const { 895 | rows: [breach] 896 | } = await fastify.pg.query( 897 | SQL`SELECT * FROM databreachrecords WHERE password=${password}` 898 | ) 899 | 900 | if (breach) { 901 | res.send( 902 | errors.BadRequest( 903 | `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.` 904 | ) 905 | ) 906 | } 907 | ``` 908 | 909 |
910 | 911 | --- 912 | 913 | # A08 Software and Data Integrity Failures 914 | 915 |
916 | 917 | - Code and infrastructure not protected against **integrity violations** 918 | - Attackers gaining access to a plugin and deploy an unverified update which would get distributed to all its users 919 | 920 |
921 | 922 | --- 923 | 924 | # A08 Common Vulnerabilities 925 | 926 |
927 | 928 | - Libraries coming from **untrusted sources**, repos or CDNs 929 | - 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** 930 | - An insecure CI/CD pipeline 931 | - Updates downloaded without sufficient integrity verification 932 | 933 |
934 | 935 | --- 936 | 937 | # A08 How to prevent 938 | 939 |
940 | 941 | - Using digital signatures for integrity checks on data and downloaded software 942 | - Ensure **npm dependencies** are trusted. For higher risks, host a custom repository of packages with internally vetted dependencies 943 | - Use automated tools to verify that components don't contain known vulnerabilities 944 | - Ensure there are code reviews for changes to minimise the risk of malicious code being introduced 945 | 946 |
947 | 948 | --- 949 | 950 | # A08 Exercise: Insecure Deserialization 951 | 952 |
953 | 954 | - Run the server for step 8 (`cd src/a08-software-data-integrity-failures`, `npm start`) 955 | - 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 request `status code 500` being returned 956 | - This is happening because the server is deserializing a `cookie containing a malicious JavaScript` code which is forcing the server to throw an exception 957 | 958 |
959 | 960 | --- 961 | 962 | # A08 Exercise: Insecure Deserialization 963 | 964 |
965 | 966 | - When decoding the cookie attached to the `/profile` request, this is what the output looks like: 967 | 968 | ```javascript 969 | // base64 to ASCII 970 | { 971 | id: 1, 972 | username: 973 | "_$$ND_FUNC$$_function () {\n throw new Error('server error')\n }()" 974 | } 975 | ``` 976 | 977 | - 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 978 | - The full step by step to serialize a JavaScript code and inject it as a cookie can be found [in this article](https://opsecx.com/index.php/2017/02/08/exploiting-node-js-deserialization-bug-for-remote-code-execution/) 979 | 980 |
981 | 982 | --- 983 | 984 | # A08 Fixing it 🪄 985 | 986 |
987 | 988 | - Run the automated tests for step 8 - `npm run verify` 989 | - The tests fail because the server shouldn't trust a library that provides a way to deserialize strings into executable JavaScript code 990 | - Untrusted data passed into `unserialize()` function in `node-serialize` module can be exploited to achieve arbitrary code execution by passing a serialized JavaScript Object with an Immediately invoked function expression (IIFE) 991 | - 💡 `JSON.parse` is a safer way to deserialize data 992 | 993 |
994 | 995 | --- 996 | 997 | # A08 Solution 💡 998 | 999 |
1000 | 1001 | ```js 1002 | export default async function solution(fastify) { 1003 | fastify.get('/profile', req => { 1004 | const cookieAsStr = Buffer.from(req.cookies.profile, 'base64').toString( 1005 | 'ascii' 1006 | ) 1007 | 1008 | const profile = JSON.parse(cookieAsStr) 1009 | 1010 | if (profile.username) { 1011 | return 'Hello ' + profile.username 1012 | } 1013 | 1014 | return 'Hello guest' 1015 | }) 1016 | } 1017 | ``` 1018 | 1019 |
1020 | 1021 | --- 1022 | 1023 | # A09 Security Logging and Monitoring Failures 1024 | 1025 |
1026 | 1027 | - Proper logging and monitoring is **critical** to detecting and responding to breaches 1028 | - It is important for alerting, accountability, visibility and forensics of security incidents 1029 | 1030 |
1031 | 1032 | --- 1033 | 1034 | # A09 Common Vulnerabilities 1035 | 1036 |
1037 | 1038 | - Auditable events, such as logins, failed logins, and **high-value transactions**, are not logged 1039 | - Warnings and errors generate no, inadequate, or unclear log messages 1040 | - Logs of applications and APIs are not monitored for **suspicious activity** 1041 | - The application cannot detect, escalate, or alert for active attacks in real-time or near real-time 1042 | 1043 |
1044 | 1045 | --- 1046 | 1047 | # A09 How to prevent 1048 | 1049 |
1050 | 1051 | - Ensure all login, access control, and server-side input validation failures can be logged with **sufficient user context** to identify suspicious or malicious accounts 1052 | - Ensure log data is encoded correctly to prevent injections or attacks on the logging or monitoring systems 1053 | - Ensure high-value transactions have an audit trail with integrity controls to prevent tampering or deletion, such as append-only database tables or similar. 1054 | 1055 |
1056 | 1057 | --- 1058 | 1059 | # A09 Suspicious activity 🤨 1060 | 1061 |
1062 | 1063 | - Run the server for step 5 (`cd src/a09-security-logging`, `npm run verify`) 1064 | - You can observe a log message `something suspicious is happening` 1065 | - Note that the application is using a vulnerable version of the http client **[undici](https://github.com/nodejs/undici/security/advisories/GHSA-f772-66g8-q5h3)** 1066 | 1067 |
1068 | 1069 | --- 1070 | 1071 | # A09 Fixing it 🪄 1072 | 1073 |
1074 | 1075 | - 💡 Log user input to identify suspicious or malicious access 1076 | - Validate user input 1077 | 1078 |
1079 | 1080 | --- 1081 | 1082 | # A09 Solution 💡 1083 | 1084 |
1085 | 1086 | ```js 1087 | // profile route handler 1088 | 1089 | async req => { 1090 | console.log({ 1091 | username: req.user.username, // add context to logs to help identify the user 1092 | input: req.headers['content-type'] 1093 | }) 1094 | const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ // validate user input 1095 | if (headerCharRegex.exec(req.headers['content-type']) !== null) { 1096 | throw errors.BadRequest() 1097 | } 1098 | const { body } = await request('http://localhost:3001', { 1099 | method: 'GET', 1100 | headers: { 1101 | 'content-type': req.headers['content-type'] 1102 | } 1103 | }) 1104 | return body 1105 | } 1106 | ``` 1107 | 1108 |
1109 | 1110 | --- 1111 | 1112 | # A10 Server Side Request Forgery 1113 | 1114 |
1115 | 1116 | - SSRF flaws occur whenever a web application is fetching a remote resource without **validating** the user-supplied URL 1117 | - 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 1118 | 1119 |
1120 | 1121 | --- 1122 | 1123 | # A10 How to prevent 1124 | 1125 |
1126 | 1127 | - **Sanitize and validate** all client-supplied input data 1128 | - Enforce the URL schema, port, and destination with a positive allow list 1129 | - Do not send raw responses to clients 1130 | - Disable HTTP redirections 1131 | - ❗ Do not mitigate SSRF via the use of a deny list or regular expression 1132 | 1133 |
1134 | 1135 | --- 1136 | 1137 | # A10 The Attack 🥷 1138 | 1139 |
1140 | 1141 | - Run the server for step 10 (`cd src/a10-server-side-request-forgery`, `npm start`) 1142 | - In Postman, run the query for `A10: Upload Image`. Observe an image being returned 1143 | - Try to run the query for `A10: Malicious Image url`. Observe `something suspicious is happening` being returned 1144 | - The server is not sanitizing user input so it will send a request to whatever url provided in the payload 1145 | 1146 |
1147 | 1148 | --- 1149 | 1150 | # A10 Fixing it 🪄 1151 | 1152 |
1153 | 1154 | - Sanitize the url making sure it's valid 1155 | - Create a whitelist of allowed domains by adding them in the database column `allowedImageDomain` 1156 | - 💡 Make sure the requested domain is in the whitelist 1157 | 1158 |
1159 | 1160 | --- 1161 | 1162 | # A10 Solution 💡 1163 | 1164 |
1165 | 1166 | ```js 1167 | export default async function profilePicture(fastify) { 1168 | fastify.post( 1169 | '/user/image', 1170 | { 1171 | onRequest: [fastify.authenticate] 1172 | }, 1173 | async req => { 1174 | const { imgUrl } = req.body 1175 | const url = validateUrl(imgUrl) // validate url using the URL object 1176 | const { 1177 | rows: [whitelisted] 1178 | } = await fastify.pg.query( 1179 | SQL`SELECT * FROM allowedImageDomain WHERE hostname = ${url.hostname}` 1180 | ) 1181 | if (!whitelisted) { 1182 | throw errors.Forbidden() 1183 | } 1184 | const { data } = await axios.get(url.href) 1185 | return data 1186 | } 1187 | ) 1188 | } 1189 | ``` 1190 | 1191 |
1192 | 1193 | --- 1194 | 1195 | # Beyond the Top 10 1196 | 1197 |
1198 | 1199 | - The OWASP Top 10 is a good introduction to the most common issues to keep in mind while developing 1200 | - By its nature, the Top 10 is a series of guidelines but can't be tested easily as its applications are broad 1201 | - On the other hand the [OWASP Application Security Verification Standard](https://owasp.org/www-project-application-security-verification-standard/) is a comprehensive list of security criteria 1202 | - Those are specific, standardised and verifiable security requirements on which on app can be tested to either pass or fail 1203 | 1204 |
1205 | 1206 | --- 1207 | 1208 | # Other useful OWASP resources 1209 | 1210 |
1211 | 1212 | - The [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org) provides condensed information on specific security topics related to OWASP standards 1213 | - The [OWASP Software Assurance Maturity Model](https://owaspsamm.org) can help an organization analyze and improve their software security 1214 | 1215 |
1216 | 1217 | --- 1218 | 1219 | # Thanks For Having Us! 1220 | 1221 | ## 👏👏👏 1222 | --------------------------------------------------------------------------------