├── 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 |  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 | [](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
13 |
14 |