├── LICENSE
├── README.md
├── client
├── .dockerignore
├── .gitignore
├── Dockerfile
├── package-lock.json
├── package.json
├── public
│ ├── index.html
│ └── manifest.json
└── src
│ ├── .DS_Store
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── components
│ ├── .DS_Store
│ ├── about.jsx
│ ├── admin.jsx
│ ├── authors.jsx
│ ├── bookForm.jsx
│ ├── books.jsx
│ ├── booksTable.jsx
│ ├── changePasswordForm.jsx
│ ├── common
│ │ ├── adminProtectedRoute.jsx
│ │ ├── footer.jsx
│ │ ├── form.jsx
│ │ ├── home.jsx
│ │ ├── input.jsx
│ │ ├── pagination.jsx
│ │ ├── protectedRoute.jsx
│ │ ├── select.jsx
│ │ ├── table.jsx
│ │ ├── tableBody.jsx
│ │ └── tableHeader.jsx
│ ├── editAuthor.jsx
│ ├── editBook.jsx
│ ├── editProfile.jsx
│ ├── loginForm.jsx
│ ├── logoutForm.jsx
│ ├── navBar.jsx
│ ├── newAuthor.jsx
│ ├── notFound.jsx
│ ├── profile.jsx
│ ├── resetPassword.jsx
│ ├── searchBox.jsx
│ ├── signUpForm.jsx
│ └── users.jsx
│ ├── config.json
│ ├── index.css
│ ├── index.js
│ ├── public
│ ├── 404.png
│ ├── back.jpg
│ ├── logo-home.png
│ ├── logo.png
│ ├── owasp-brand.png
│ ├── owasp.jpg
│ └── rest.png
│ ├── registerServiceWorker.js
│ ├── services
│ ├── authService.js
│ ├── authorService.js
│ ├── bookService.js
│ ├── categoryService.js
│ ├── httpService.js
│ ├── thirdParty.js
│ └── userService.js
│ └── utils
│ └── paginate.js
├── docker-compose.yml
├── server
├── .dockerignore
├── .gitignore
├── Dockerfile
├── cert
│ ├── private_key.key
│ └── pub_key.crt
├── config
│ ├── custom-environment-variables.json
│ ├── default.json
│ └── test.json
├── docker-entrypoint.sh
├── index.js
├── logfile.log
├── middleware
│ ├── admin.js
│ ├── auth.js
│ ├── cache.js
│ ├── error.js
│ └── validateObjectId.js
├── migrate-mongo-config.js
├── migrations
│ ├── 20230820084942-authors.js
│ ├── 20230820161341-books.js
│ ├── 20230820161349-categories.js
│ └── 20230820171849-users.js
├── models
│ ├── author.js
│ ├── book.js
│ ├── category.js
│ ├── token.js
│ └── user.js
├── package-lock.json
├── package.json
├── routes
│ ├── adminAuth.js
│ ├── auth.js
│ ├── authors.js
│ ├── books.js
│ ├── categories.js
│ ├── log.js
│ ├── me.js
│ ├── system.js
│ ├── users.js
│ └── usersDev.js
├── smtp_config.list
├── startup
│ ├── db.js
│ ├── logging.js
│ └── routes.js
├── utils
│ └── sendEmail.js
└── wait-for
└── vulnerabilities
└── vulnerabilities.md
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Borna Nematzadeh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vulnerable REST API - OWASP 2023
2 |
3 | A vulnerable RESTful application written in Node and React based on [OWASP API security top 10 2023 edition](https://owasp.org/API-Security/editions/2023/en/0x11-t10/). Common API vulnerabilities and attack scenarios are also included to make it more comprehensive.
4 |
5 | 
6 |
7 | # How to install
8 | ## Option 1: Run on Docker (recommended)
9 | ```
10 | git clone https://github.com/bnematzadeh/vulnerable-rest-api.git
11 | ```
12 |
13 | ```
14 | cd vulnerable-rest-api/
15 | ```
16 |
17 | Add your SMTP credentials and provider in server/smtp_config.list. I personally recommend the [elasticemail](https://elasticemail.com/):
18 |
19 | - Create an account
20 | - Navigate to settings/Create SMTP Credentials
21 | - Edit "server/smtp_config.list" and add your credentials
22 |
23 | ```
24 | docker-compose up --build
25 | ```
26 | Access the Application
27 |
28 | - Client: localhost:3000
29 | - API: localhost:3001
30 |
31 | ## Option 2: Run on Your Machine (Manual Installation)
32 | Make sure you have already installed Node and MongoDB on your system.
33 | - Client Setup
34 | - ```cd client/ ```
35 | - ```npm install```
36 | - ```npm start```
37 | - Access the application via http://localhost:3000
38 | - Server Setup
39 | - Set your SMTP credentials as environment variables based on server/config/custom-environment-variables.json
40 | - Make sure the mongodb service is running
41 | - ```cd server/ ```
42 | - ```npm install```
43 | - ```npm install migrate-mongo --dev-save && npm run db:up```
44 | - ```npm start```
45 | - The API is available on http://localhost:3001
46 |
47 | # Vulnerabilities
48 | - API1:2023 - Broken Object Level Authorization
49 | - API2:2023 - Broken Authentication
50 | - API3:2023 - Broken Object Property Level Authorization
51 | - API4:2023 - Unrestricted Resource Consumption
52 | - API5:2023 - Broken Function Level Authorization
53 | - API6:2023 - Unrestricted Access to Sensitive Business Flows
54 | - API7:2023 - Server Side Request Forgery
55 | - API8:2023 - Security Misconfiguration
56 | - API9:2023 - Improper Inventory Management
57 | - API10:2023 - Unsafe Consumption of APIs
58 | - Bonus
59 | - Injection
60 | - Web Cache Deception
61 | - Weak Implementation of Reset Password (Account Takeover)
62 |
63 |
--------------------------------------------------------------------------------
/client/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/client/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18.17.1-alpine3.18
2 |
3 | WORKDIR /app
4 | COPY package*.json ./
5 | RUN npm install
6 | COPY . .
7 |
8 | EXPOSE 3000
9 |
10 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vulnerable-api-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fortawesome/fontawesome-svg-core": "^6.4.0",
7 | "@fortawesome/free-solid-svg-icons": "^6.4.0",
8 | "@fortawesome/react-fontawesome": "^0.2.0",
9 | "axios": "^1.4.0",
10 | "bootstrap": "^4.1.1",
11 | "font-awesome": "^4.7.0",
12 | "joi-browser": "^13.4.0",
13 | "jwt-decode": "^3.1.2",
14 | "lodash": "^4.17.21",
15 | "prop-types": "^15.6.2",
16 | "query-string": "^8.1.0",
17 | "react": "^18.2.0",
18 | "react-dom": "^16.4.1",
19 | "react-notification-system": "^0.4.0",
20 | "react-router-dom": "^4.3.1",
21 | "react-scripts": "^5.0.1",
22 | "react-social-icons": "^5.15.0"
23 | },
24 | "scripts": {
25 | "start": "react-scripts start",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test --env=jsdom",
28 | "eject": "react-scripts eject"
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">0.2%",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | Vulnerable REST API
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bnematzadeh/vulnerable-rest-api/33467261f3ec6a2b0aa9b9ff08c30a39a5572b9b/client/src/.DS_Store
--------------------------------------------------------------------------------
/client/src/App.css:
--------------------------------------------------------------------------------
1 | .starter-template {
2 | padding: 2rem 1.5rem;
3 | text-align: center;
4 | }
5 |
6 | form.edit{
7 | max-width: 400px;
8 | margin: 0 auto;
9 | }
10 |
11 | form.edit h1{
12 | text-align: center;
13 | }
14 |
15 | form.login, form.register, form.about{
16 | max-width: 400px;
17 | margin: 0 auto;
18 | }
19 | input{
20 | display: block;
21 | margin-bottom: 5px;
22 | width: 100%;
23 | padding: 5px 7px;
24 | border: 2px solid #ddd;
25 | border-radius: 5px;
26 | background-color: #fff;
27 | }
28 |
29 | button{
30 | cursor: pointer;
31 | background-color: #555;
32 | border:0;
33 | color: #fff;
34 | border-radius: 5px;
35 | padding: 7px 0;
36 | }
37 |
38 | form.login h1, form.register h1, form.about{
39 | text-align: center;
40 | margin-top: 30px;
41 | }
42 |
43 | .profile li, h1, h2, p{
44 | font-family:'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
45 | font-size: 20px;
46 | font-weight: normal;
47 | margin: 15px;
48 | }
49 |
50 | .books, .authors, .about, .users{
51 | margin-top: 40px;
52 | }
53 |
54 | tr{
55 | font-family:'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
56 | }
57 |
58 | nav{
59 | font-family:'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
60 | }
61 |
62 |
63 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Route, Redirect, Switch } from "react-router-dom";
3 | import ProtectedRoute from "./components/common/protectedRoute";
4 | import Books from "./components/books";
5 | import BookForm from "./components/bookForm";
6 | import EditBook from "./components/editBook";
7 | import Authors from './components/authors';
8 | import EditAuthor from './components/editAuthor';
9 | import Users from './components/users';
10 | import NewAuthor from "./components/newAuthor";
11 | import NotFound from "./components/notFound";
12 | import NavBar from "./components/navBar";
13 | import LoginForm from "./components/loginForm";
14 | import LogoutForm from './components/logoutForm';
15 | import ResetPasswordForm from "./components/resetPassword";
16 | import ChangePasswordForm from "./components/changePasswordForm";
17 | import signUpForm from "./components/signUpForm";
18 | import Home from "./components/common/home";
19 | import Footer from "./components/common/footer";
20 | import About from "./components/about";
21 | import AdminLogin from "./components/admin";
22 | import AdminProtectedRoute from './components/common/adminProtectedRoute';
23 | import Profile from "./components/profile";
24 | import EditProfile from "./components/editProfile";
25 | import auth from "./services/authService";
26 | import "./App.css";
27 |
28 |
29 |
30 |
31 |
32 | class App extends Component {
33 | state = {}
34 |
35 | componentDidMount() {
36 | const user = auth.getUser();
37 | this.setState({user});
38 | }
39 |
40 | render() {
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | }/>
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | );
71 | }
72 | }
73 |
74 | export default App;
75 |
--------------------------------------------------------------------------------
/client/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/client/src/components/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bnematzadeh/vulnerable-rest-api/33467261f3ec6a2b0aa9b9ff08c30a39a5572b9b/client/src/components/.DS_Store
--------------------------------------------------------------------------------
/client/src/components/about.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import NotificationSystem from 'react-notification-system';
3 | import { addSubscriber } from '../services/thirdParty';
4 |
5 | class About extends Component{
6 | notificationSystem = React.createRef();
7 | state= {
8 | user: [],
9 | result: ''
10 | }
11 |
12 |
13 | async componentDidMount() {
14 | const query = this.props.location.search;
15 | const params = new URLSearchParams(query);
16 | if(params.get('email')){
17 | try{
18 | const {data} = await addSubscriber(params.get('email'));
19 | this.setState({result: data.email});
20 | }catch(ex){
21 | notification.addNotification({
22 | message: 'Try again later',
23 | level: 'error'
24 | });
25 | }
26 | }
27 | }
28 |
29 | handleChange = ({ currentTarget: input }) => {
30 | const user = { ...this.state.user };
31 | user[input.name] = input.value;
32 | this.setState({ user });
33 | };
34 |
35 | handleSubmit = async e => {
36 | e.preventDefault();
37 | const notification = this.notificationSystem.current;
38 | try{
39 | const {email} = this.state.user;
40 | const {data} = await addSubscriber(email);
41 | this.setState({result: data.email});
42 | }catch(ex){
43 | notification.addNotification({
44 | message: 'Try again later',
45 | level: 'error'
46 | });
47 | }
48 | };
49 |
50 | render(){
51 | const {result} = this.state;
52 | return (
53 |
54 |
55 |
What is the Vulnerable REST API ?
56 |
57 |
In this project, you'll need to identify the API vulnerabilities based on OWASP API Security Top 10 - 2023
58 |
59 |
60 |
65 | {result && (
66 |
69 | )}
70 |
71 |
72 | );
73 | }
74 |
75 | }
76 |
77 | export default About;
--------------------------------------------------------------------------------
/client/src/components/admin.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Joi from "joi-browser";
3 | import NotificationSystem from 'react-notification-system';
4 | import Form from "./common/form";
5 | import auth from '../services/authService';
6 |
7 | class AdminLogin extends Form {
8 | notificationSystem = React.createRef();
9 | state = {
10 | data: { username: "", password: "" },
11 | errors: {}
12 | };
13 |
14 | schema = {
15 | username: Joi.string()
16 | .required()
17 | .label("Username"),
18 | password: Joi.string()
19 | .required()
20 | .label("Password")
21 | };
22 |
23 | doSubmit = async () => {
24 | const notification = this.notificationSystem.current;
25 | try{
26 |
27 | const {data} = this.state;
28 | const {state} = this.props.location;
29 | await auth.adminLogin(data.username, data.password);
30 |
31 | notification.addNotification({
32 | message: 'Logged-in Successfully',
33 | level: 'success'
34 | });
35 |
36 | window.setTimeout(()=>{
37 | window.location = state ? state.from.pathname : '/profile/users';
38 | },2000)
39 |
40 | }catch(ex){
41 | if(ex.response || ex.response.status === 403 || ex.response.status === 400){
42 | notification.addNotification({
43 | message: ex.response.data,
44 | level: 'error'
45 | });
46 | }
47 | }
48 | };
49 |
50 | render() {
51 | return (
52 |
63 | );
64 | }
65 | }
66 |
67 | export default AdminLogin;
68 |
--------------------------------------------------------------------------------
/client/src/components/authors.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Link } from "react-router-dom";
3 | import _ from 'lodash';
4 | import NotificationSystem from 'react-notification-system';
5 | import Pagination from "./common/pagination";
6 | import { paginate } from "../utils/paginate";
7 | import Table from "./common/table";
8 | import { getAuthors, deleteAuthor } from "../services/authorService";
9 | import auth from '../services/authService';
10 |
11 | class Authors extends Component {
12 | notificationSystem = React.createRef();
13 | constructor(){
14 | super();
15 | const user = auth.getUser();
16 | if(user && user.role == 'ADMIN'){
17 | this.columns.push(this.updateColumn,this.deleteColumn);
18 | }
19 | }
20 |
21 | updateColumn = {
22 | key: "update",
23 | content: author => (
24 |
25 |
27 |
28 |
29 | )
30 | }
31 |
32 | deleteColumn = {
33 | key: "delete",
34 | content: author => (
35 |
41 | )
42 | }
43 | columns = [
44 | {
45 | path: "name",
46 | label: "Name"
47 | },
48 | { path: "about", label: "About" },
49 | { path: "job", label: "Job" }
50 | ]
51 |
52 | state = {
53 | authors : [],
54 | currentPage: 1,
55 | pageSize: 3,
56 | sortColumn: { path: "title", order: "asc" }
57 | }
58 |
59 | handleDelete = async author => {
60 | const notification = this.notificationSystem.current;
61 | const authors = this.state.authors.filter(m => m._id !== author._id);
62 | this.setState({ authors });
63 | try{
64 | await deleteAuthor(author._id);
65 | notification.addNotification({
66 | message: 'Deleted Successfully',
67 | level: 'success'
68 | });
69 | }catch(ex){
70 | if(ex.response && ex.response.status === 400){
71 | notification.addNotification({
72 | message: ex.response.data,
73 | level: 'error'
74 | });
75 | }
76 | };
77 | }
78 |
79 | handleSort = sortColumn => {
80 | this.setState({ sortColumn });
81 | };
82 |
83 | getSortedData = ()=>{
84 | const {sortColumn} = this.state;
85 | const sorted = _.orderBy(this.state.authors, [sortColumn.path], [sortColumn.order]);
86 | return sorted;
87 | }
88 |
89 | async componentDidMount(){
90 | const {data} = await getAuthors();
91 | this.setState({authors: data});
92 | }
93 |
94 | handlePageChange = page => {
95 | this.setState({ currentPage: page });
96 | };
97 |
98 | getPagedData = () => {
99 | const {
100 | pageSize,
101 | currentPage,
102 | sortColumn,
103 | authors: allAuthors
104 | } = this.state;
105 |
106 | const sorted = _.orderBy(allAuthors, [sortColumn.path], [sortColumn.order]);
107 |
108 | const authors = paginate(sorted, currentPage, pageSize);
109 |
110 | return { data: authors, totalCount: allAuthors.length };
111 | };
112 |
113 |
114 | render(){
115 | const {data, totalCount} = this.getPagedData();
116 | const {pageSize, currentPage} = this.state;
117 | return (
118 |
136 | );
137 | }
138 | };
139 |
140 | export default Authors;
141 |
--------------------------------------------------------------------------------
/client/src/components/bookForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Joi from 'joi-browser';
3 | import NotificationSystem from 'react-notification-system';
4 | import Form from './common/form';
5 | import { getCategories } from '../services/categoryService';
6 | import { addBook } from '../services/bookService';
7 | import {getAuthors} from '../services/authorService';
8 |
9 | class BookForm extends Form {
10 | notificationSystem = React.createRef();
11 | state = {
12 | data: { title: "", author: "", category: "", publishedDate: "" },
13 | errors: {},
14 | authors: [],
15 | categories: []
16 | };
17 |
18 | async componentDidMount() {
19 | const {data: categories} = await getCategories();
20 | const {data: authors} = await getAuthors();
21 | this.setState({categories, authors});
22 | }
23 |
24 | schema = {
25 | title: Joi.string()
26 | .required()
27 | .min(4)
28 | .label("Title"),
29 | category: Joi.string()
30 | .required(),
31 | author: Joi.string()
32 | .required()
33 | .min(4)
34 | .label("Author"),
35 | publishedDate: Joi.string()
36 | .required()
37 | .label("Date")
38 | };
39 |
40 | doSubmit = async () => {
41 | const notification = this.notificationSystem.current;
42 | try{
43 | const {data} = this.state;
44 | await addBook(data);
45 | notification.addNotification({
46 | message: 'Added Successfully!',
47 | level: 'success'
48 | });
49 | }catch(ex){
50 | if(ex.response && ex.response.status === 400){
51 | notification.addNotification({
52 | message: ex.response.data,
53 | level: 'error'
54 | });
55 | }
56 | }
57 | };
58 |
59 | render() {
60 | return (
61 |
74 | );
75 | }
76 | }
77 |
78 | export default BookForm;
--------------------------------------------------------------------------------
/client/src/components/books.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import _ from "lodash";
3 | import NotificationSystem from 'react-notification-system';
4 | import BooksTable from "./booksTable";
5 | import Pagination from "./common/pagination";
6 | import { getBooks, deleteBook } from '../services/bookService';
7 | import { getCategories } from "../services/categoryService";
8 | import { paginate } from "../utils/paginate";
9 | import SearchBox from "./searchBox";
10 |
11 | class Books extends Component {
12 | notificationSystem = React.createRef();
13 | state = {
14 | books: [],
15 | categories: [],
16 | currentPage: 1,
17 | pageSize: 5,
18 | searchQuery: "",
19 | sortColumn: { path: "title", order: "asc" }
20 | };
21 |
22 |
23 | async componentDidMount() {
24 | const {data: categories} = await getCategories();
25 | const {data: books} = await getBooks();
26 |
27 | this.setState({ books , categories });
28 | }
29 |
30 | handleDelete = async book => {
31 | const notification = this.notificationSystem.current;
32 | const books = this.state.books.filter(m => m._id !== book._id);
33 | this.setState({ books });
34 |
35 | try{
36 | await deleteBook(book._id);
37 | notification.addNotification({
38 | message: 'Deleted Successfully',
39 | level: 'success'
40 | });
41 | }catch(ex){
42 | if(ex.response && ex.response.status === 400){
43 | notification.addNotification({
44 | message: ex.response.data,
45 | level: 'error'
46 | });
47 | }
48 | };
49 | }
50 |
51 | handlePageChange = page => {
52 | this.setState({ currentPage: page });
53 | };
54 |
55 | handleSearch = query => {
56 | this.setState({ searchQuery: query, currentPage: 1 });
57 | };
58 |
59 | handleSort = sortColumn => {
60 | this.setState({ sortColumn });
61 | };
62 |
63 | getPagedData = () => {
64 | const {
65 | pageSize,
66 | currentPage,
67 | sortColumn,
68 | searchQuery,
69 | books: allBooks
70 | } = this.state;
71 |
72 | let filtered = allBooks;
73 | if (searchQuery)
74 | filtered = allBooks.filter(m =>
75 | m.title.toLowerCase().startsWith(searchQuery.toLowerCase())
76 | );
77 |
78 | const sorted = _.orderBy(filtered, [sortColumn.path], [sortColumn.order]);
79 |
80 | const books = paginate(sorted, currentPage, pageSize);
81 |
82 | return { totalCount: filtered.length, data: books };
83 | };
84 |
85 | render() {
86 | const { length: count } = this.state.books;
87 | const { pageSize, currentPage, sortColumn, searchQuery } = this.state;
88 |
89 | if (count === 0) return There are no books
;
90 |
91 | const { totalCount, data: books } = this.getPagedData();
92 |
93 | return (
94 |
95 |
96 |
Showing {totalCount} Books
97 |
98 |
104 |
105 |
111 |
112 |
113 |
114 | );
115 | }
116 | }
117 |
118 | export default Books;
119 |
--------------------------------------------------------------------------------
/client/src/components/booksTable.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Link from "react-router-dom/Link";
3 | import Table from "./common/table";
4 | import auth from '../services/authService';
5 |
6 | class booksTable extends Component {
7 |
8 | constructor(){
9 | super();
10 | const user = auth.getUser();
11 | if(user && user.role == 'ADMIN'){
12 | this.columns.push(this.updateColumn, this.deleteColumn);
13 | }
14 | }
15 |
16 | updateColumn = {
17 | key: "update",
18 | content: book => (
19 |
20 |
22 |
23 |
24 | )
25 | }
26 |
27 | deleteColumn = {
28 | key: "delete",
29 | content: book => (
30 |
36 | )
37 | }
38 |
39 | columns = [
40 | {
41 | path: "title",
42 | label: "Title"
43 | },
44 | {
45 | path: "author.name",
46 | label: "Author"
47 | },
48 | { path: "category.name", label: "Category" },
49 | { path: "publishedDate", label: "Published Date" }
50 | ];
51 |
52 | render() {
53 | const { books, onSort, sortColumn } = this.props;
54 |
55 | return (
56 |
62 | );
63 | }
64 | }
65 |
66 | export default booksTable;
67 |
--------------------------------------------------------------------------------
/client/src/components/changePasswordForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Redirect} from "react-router-dom";
3 | import Joi from "joi-browser";
4 | import NotificationSystem from 'react-notification-system';
5 | import Form from "./common/form";
6 | import {changePassword} from '../services/userService';
7 | import auth from "../services/authService";
8 |
9 | class changePasswordForm extends Form{
10 | notificationSystem = React.createRef();
11 | state = {
12 | data: { value: ""},
13 | errors: {}
14 | }
15 |
16 | schema = {
17 | value: Joi.string()
18 | .min(5)
19 | .required()
20 | };
21 |
22 | doSubmit = async () => {
23 | const notification = this.notificationSystem.current;
24 | try{
25 | const {data: newPass} = this.state;
26 | const data = this.props.location.search;
27 | const params = new URLSearchParams(data);
28 | if(params.get('userId') && params.get('token')){
29 | let user = Object.fromEntries(params.entries())
30 | await changePassword(newPass, user);
31 | notification.addNotification({
32 | message: 'Updated Successfully!',
33 | level: 'success'
34 | });
35 | window.setTimeout(()=>{
36 | window.location = '/login';
37 | },2000)
38 | }else{
39 | notification.addNotification({
40 | message: 'userId or token are not allowed to be empty!',
41 | level: 'error'
42 | });
43 | }
44 |
45 | }catch(ex){
46 | if(ex.response && ex.response.status === 401){
47 | notification.addNotification({
48 | message: ex.response.data,
49 | level: 'error'
50 | });
51 | }
52 | }
53 | };
54 |
55 | render(){
56 | if(auth.getUser()) return
57 | return (
58 |
68 | );
69 | }
70 | }
71 |
72 | export default changePasswordForm;
--------------------------------------------------------------------------------
/client/src/components/common/adminProtectedRoute.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Redirect } from 'react-router-dom';
3 | import auth from '../../services/authService';
4 |
5 | const AdminProtectedRoute = ({path, component:Component, render, ...rest }) => {
6 | return (
7 | {
10 | if(!auth.getUser() || auth.getUser().role !== 'ADMIN') return ;
14 | return Component ? : render(props);
15 | }} />
16 | );
17 | }
18 |
19 | export default AdminProtectedRoute;
--------------------------------------------------------------------------------
/client/src/components/common/footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SocialIcon } from 'react-social-icons';
3 |
4 | const Footer = () =>{
5 | return(
6 |
13 | );
14 | }
15 |
16 | export default Footer;
--------------------------------------------------------------------------------
/client/src/components/common/form.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Joi from "joi-browser";
3 | import Input from "./input";
4 | import Select from "./select";
5 |
6 | class Form extends Component {
7 | state = {
8 | data: {},
9 | errors: {}
10 | };
11 |
12 | validate = () => {
13 | const options = { abortEarly: false };
14 | const { error } = Joi.validate(this.state.data, this.schema, options);
15 | if (!error) return null;
16 |
17 | const errors = {};
18 | for (let item of error.details) errors[item.path[0]] = item.message;
19 | return errors;
20 | };
21 |
22 | validateProperty = ({ name, value }) => {
23 | const obj = { [name]: value };
24 | const schema = { [name]: this.schema[name] };
25 | const { error } = Joi.validate(obj, schema);
26 | return error ? error.details[0].message : null;
27 | };
28 |
29 | handleSubmit = e => {
30 | e.preventDefault();
31 |
32 | const errors = this.validate();
33 | this.setState({ errors: errors || {} });
34 | if (errors) return;
35 |
36 | this.doSubmit();
37 | };
38 |
39 | handleChange = ({ currentTarget: input }) => {
40 | const errors = { ...this.state.errors };
41 | const errorMessage = this.validateProperty(input);
42 | if (errorMessage) errors[input.name] = errorMessage;
43 | else delete errors[input.name];
44 |
45 | const data = { ...this.state.data };
46 | data[input.name] = input.value;
47 |
48 | this.setState({ data, errors });
49 | };
50 |
51 | renderButton(label) {
52 | return (
53 |
56 | );
57 | }
58 |
59 | renderSelect(name, label, options) {
60 | const { data, errors } = this.state;
61 |
62 | return (
63 |
71 | );
72 | }
73 |
74 | renderInput(name, label, type = "text") {
75 | const { data, errors } = this.state;
76 |
77 | return (
78 |
86 | );
87 | }
88 | }
89 |
90 | export default Form;
91 |
--------------------------------------------------------------------------------
/client/src/components/common/home.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import logo2 from '../../public/owasp.jpg';
3 |
4 | const Home = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | export default Home;
16 |
--------------------------------------------------------------------------------
/client/src/components/common/input.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Input = ({ name, label, error, ...rest }) => {
4 | return (
5 |
6 |
7 |
8 | {error &&
{error}
}
9 |
10 | );
11 | };
12 |
13 | export default Input;
14 |
--------------------------------------------------------------------------------
/client/src/components/common/pagination.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import _ from "lodash";
4 |
5 | const Pagination = ({ itemsCount, pageSize, currentPage, onPageChange }) => {
6 | const pagesCount = Math.ceil(itemsCount / pageSize);
7 | if (pagesCount === 1) return null;
8 | const pages = _.range(1, pagesCount + 1);
9 |
10 | return (
11 |
25 | );
26 | };
27 |
28 | Pagination.propTypes = {
29 | itemsCount: PropTypes.number.isRequired,
30 | pageSize: PropTypes.number.isRequired,
31 | currentPage: PropTypes.number.isRequired,
32 | onPageChange: PropTypes.func.isRequired
33 | };
34 |
35 | export default Pagination;
36 |
--------------------------------------------------------------------------------
/client/src/components/common/protectedRoute.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Redirect } from 'react-router-dom';
3 | import auth from '../../services/authService';
4 |
5 | const ProtectedRoute = ({path, component:Component, render, ...rest }) => {
6 | return (
7 | {
10 | if(!auth.getUser()) return ;
14 | return Component ? : render(props);
15 | }} />
16 | );
17 | }
18 |
19 | export default ProtectedRoute;
--------------------------------------------------------------------------------
/client/src/components/common/select.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Select = ({ name, label, options, error, ...rest }) => {
4 | return (
5 |
6 |
7 |
15 | {error &&
{error}
}
16 |
17 | );
18 | };
19 |
20 | export default Select;
21 |
--------------------------------------------------------------------------------
/client/src/components/common/table.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import TableHeader from "./tableHeader";
3 | import TableBody from "./tableBody";
4 |
5 | const Table = ({ columns, sortColumn, onSort, data }) => {
6 | return (
7 |
11 | );
12 | };
13 |
14 | export default Table;
15 |
--------------------------------------------------------------------------------
/client/src/components/common/tableBody.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import _ from "lodash";
3 |
4 | class TableBody extends Component {
5 | renderCell = (item, column) => {
6 | if (column.content) return column.content(item);
7 |
8 | return _.get(item, column.path);
9 | };
10 |
11 | createKey = (item, column) => {
12 | return item._id + (column.path || column.key);
13 | };
14 |
15 | render() {
16 | const { data, columns } = this.props;
17 |
18 | return (
19 |
20 | {data.map(item => (
21 |
22 | {columns.map(column => (
23 |
24 | {this.renderCell(item, column)}
25 | |
26 | ))}
27 |
28 | ))}
29 |
30 | );
31 | }
32 | }
33 |
34 | export default TableBody;
35 |
--------------------------------------------------------------------------------
/client/src/components/common/tableHeader.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | class TableHeader extends Component {
4 | raiseSort = path => {
5 | const sortColumn = { ...this.props.sortColumn };
6 | if (sortColumn.path === path)
7 | sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc";
8 | else {
9 | sortColumn.path = path;
10 | sortColumn.order = "asc";
11 | }
12 | this.props.onSort(sortColumn);
13 | };
14 |
15 | renderSortIcon = column => {
16 | const { sortColumn } = this.props;
17 |
18 | if (column.path !== sortColumn.path) return null;
19 | if (sortColumn.order === "asc") return ;
20 | return ;
21 | };
22 |
23 | render() {
24 | return (
25 |
26 |
27 | {this.props.columns.map(column => (
28 | this.raiseSort(column.path)}
32 | >
33 | {column.label} {this.renderSortIcon(column)}
34 | |
35 | ))}
36 |
37 |
38 | );
39 | }
40 | }
41 |
42 | export default TableHeader;
43 |
--------------------------------------------------------------------------------
/client/src/components/editAuthor.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Joi from 'joi-browser';
3 | import NotificationSystem from 'react-notification-system';
4 | import Form from './common/form';
5 | import { updateAuthor } from '../services/authorService';
6 |
7 | class EditAuthor extends Form {
8 | notificationSystem = React.createRef();
9 | state = {
10 | data: { name: "", email: "", about: "", job: "" },
11 | errors: {},
12 | authors: [],
13 | categories: []
14 | };
15 |
16 | schema = {
17 | name: Joi.string()
18 | .required()
19 | .min(4)
20 | .label("Name"),
21 | email: Joi.string()
22 | .email()
23 | .required(),
24 | about: Joi.string()
25 | .required()
26 | .min(5)
27 | .label("about"),
28 | job: Joi.string()
29 | .required()
30 | .label("Job")
31 | };
32 |
33 | doSubmit = async () => {
34 | const authorId = this.props.match.params.authorId;
35 | const {data: author} = this.state;
36 | const notification = this.notificationSystem.current;
37 | try{
38 | await updateAuthor(authorId, author);
39 | notification.addNotification({
40 | message: 'Updated Successfully',
41 | level: 'success'
42 | });
43 | }catch(ex){
44 | if(ex.response && ex.response.status === 400){
45 | notification.addNotification({
46 | message: ex.response.data,
47 | level: 'error'
48 | });
49 | }
50 | }
51 | };
52 |
53 | render() {
54 | return (
55 |
68 | );
69 | }
70 | }
71 |
72 | export default EditAuthor;
--------------------------------------------------------------------------------
/client/src/components/editBook.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Joi from 'joi-browser';
3 | import NotificationSystem from 'react-notification-system';
4 | import Form from './common/form';
5 | import { getCategories } from '../services/categoryService';
6 | import { updateBook } from '../services/bookService';
7 | import {getAuthors} from '../services/authorService';
8 |
9 | class EditBook extends Form {
10 | notificationSystem = React.createRef();
11 | state = {
12 | data: { title: "", author: "", category: "", publishedDate: "" },
13 | errors: {},
14 | authors: [],
15 | categories: []
16 | };
17 |
18 | async componentDidMount() {
19 | const {data: categories} = await getCategories();
20 | const {data: authors} = await getAuthors();
21 | this.setState({categories, authors});
22 | }
23 |
24 | schema = {
25 | title: Joi.string()
26 | .required()
27 | .min(4)
28 | .label("Title"),
29 | category: Joi.string()
30 | .required(),
31 | author: Joi.string()
32 | .required()
33 | .min(4)
34 | .label("Author"),
35 | publishedDate: Joi.string()
36 | .required()
37 | .label("Date")
38 | };
39 |
40 | doSubmit = async () => {
41 | const bookId = this.props.match.params.bookId;
42 | const {data: book} = this.state;
43 | const notification = this.notificationSystem.current;
44 | try{
45 | await updateBook(bookId, book);
46 | notification.addNotification({
47 | message: 'Updated Successfully!',
48 | level: 'success'
49 | });
50 | }catch(ex){
51 | if(ex.response && ex.response.status === 400){
52 | notification.addNotification({
53 | message: ex.response.data,
54 | level: 'error'
55 | });
56 | }
57 | }
58 | };
59 |
60 | render() {
61 | return (
62 |
75 | );
76 | }
77 | }
78 |
79 | export default EditBook;
--------------------------------------------------------------------------------
/client/src/components/editProfile.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Joi from 'joi-browser';
3 | import NotificationSystem from 'react-notification-system';
4 | import Form from './common/form';
5 | import { updateUser } from '../services/userService';
6 |
7 |
8 | class EditProfile extends Form {
9 | notificationSystem = React.createRef();
10 | state = {
11 | data: { name: "", email: "", url: "", currentPass: "", newPass: "" },
12 | errors: {}
13 | };
14 |
15 |
16 | schema = {
17 | name: Joi.string()
18 | .required()
19 | .label("Name"),
20 | email: Joi.string()
21 | .required()
22 | .email()
23 | .label("Email"),
24 | url: Joi.string().uri(),
25 | currentPass: Joi.string()
26 | .required()
27 | .min(5)
28 | .label("Current Password"),
29 | newPass: Joi.string()
30 | .required()
31 | .min(5)
32 | .label("New Password")
33 | };
34 |
35 | doSubmit = async () => {
36 | const notification = this.notificationSystem.current;
37 | try{
38 | const {data: user} = this.state;
39 | console.log(user);
40 | await updateUser(user);
41 | notification.addNotification({
42 | message: 'Updated Successfully',
43 | level: 'success'
44 | });
45 | }catch(ex){
46 | if(ex.response && ex.response.status === 400){
47 | notification.addNotification({
48 | message: ex.response.data,
49 | level: 'error'
50 | });
51 | }
52 | }
53 | };
54 |
55 | render() {
56 | return (
57 |
71 | );
72 | }
73 | }
74 |
75 | export default EditProfile;
--------------------------------------------------------------------------------
/client/src/components/loginForm.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {Redirect} from "react-router-dom";
3 | import Joi from "joi-browser";
4 | import NotificationSystem from 'react-notification-system';
5 | import Form from "./common/form";
6 | import auth from "../services/authService";
7 | import {Link } from "react-router-dom";
8 |
9 | class LoginForm extends Form {
10 | notificationSystem = React.createRef();
11 | state = {
12 | data: { username: "", password: "" },
13 | errors: {}
14 | };
15 |
16 | schema = {
17 | username: Joi.string()
18 | .required()
19 | .label("Username"),
20 | password: Joi.string()
21 | .required()
22 | .label("Password")
23 | };
24 |
25 | doSubmit = async () => {
26 | const notification = this.notificationSystem.current;
27 | try{
28 |
29 | const {data} = this.state;
30 | const {state} = this.props.location;
31 | await auth.login(data.username, data.password);
32 |
33 | notification.addNotification({
34 | message: 'Logged-in Successfully',
35 | level: 'success'
36 | });
37 |
38 | window.setTimeout(()=>{
39 | window.location = state ? state.from.pathname : '/';
40 | },2000)
41 |
42 | }catch(ex){
43 | if(ex.response && ex.response.status === 400){
44 | notification.addNotification({
45 | message: 'Invalid Username or Password',
46 | level: 'error'
47 | });
48 | }
49 | }
50 | };
51 |
52 | render() {
53 | if(auth.getUser()) return
54 | return (
55 |
71 | );
72 | }
73 | }
74 |
75 | export default LoginForm;
76 |
--------------------------------------------------------------------------------
/client/src/components/logoutForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import auth from '../services/authService';
3 |
4 | class LogoutForm extends Component {
5 |
6 | componentDidMount() {
7 | auth.logout();
8 | window.location = '/';
9 | }
10 |
11 | render() {
12 | return null;
13 | }
14 | }
15 |
16 | export default LogoutForm;
--------------------------------------------------------------------------------
/client/src/components/navBar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import _ from 'lodash';
3 | import { Link, NavLink } from "react-router-dom";
4 | import brand from '../public/owasp-brand.png';
5 | import auth from '../services/authService';
6 |
7 | const NavBar = ({user}) => {
8 | const role = auth.getUser() && auth.getUser().role;
9 |
10 | return (
11 |
67 | );
68 | };
69 |
70 | export default NavBar;
71 |
--------------------------------------------------------------------------------
/client/src/components/newAuthor.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Joi from 'joi-browser';
3 | import NotificationSystem from 'react-notification-system';
4 | import Form from './common/form';
5 | import { addAuthor } from '../services/authorService';
6 |
7 | class NewAuthor extends Form {
8 | notificationSystem = React.createRef();
9 |
10 | state = {
11 | data: { name: "", email: "", job: "", about: "" },
12 | errors: {}
13 | };
14 |
15 | schema = {
16 | name: Joi.string()
17 | .required()
18 | .min(4),
19 | email: Joi.string()
20 | .required(),
21 | job: Joi.string()
22 | .required()
23 | .min(4)
24 | .label("Job"),
25 | about: Joi.string()
26 | .required()
27 | .label("About")
28 | .min(5)
29 | };
30 |
31 | doSubmit = async () => {
32 | const notification = this.notificationSystem.current;
33 | try{
34 |
35 | const {data: author} = this.state;
36 | await addAuthor(author);
37 | notification.addNotification({
38 | message: 'Added Successfully!',
39 | level: 'success'
40 | });
41 |
42 | }catch(ex){
43 | if(ex.response && ex.response.status === 400){
44 | notification.addNotification({
45 | message: ex.response.data,
46 | level: 'error'
47 | });
48 | }
49 | }
50 | };
51 |
52 | render() {
53 | return (
54 |
67 | );
68 | }
69 | }
70 |
71 | export default NewAuthor;
--------------------------------------------------------------------------------
/client/src/components/notFound.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import logo from '../public/404.png';
3 |
4 | const NotFound = () => {
5 | return (
6 |
7 |

8 |
9 | );
10 | };
11 |
12 | export default NotFound;
13 |
--------------------------------------------------------------------------------
/client/src/components/profile.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import _ from 'lodash';
3 | import { getUser } from '../services/userService';
4 | import { Link, NavLink } from 'react-router-dom';
5 |
6 | class Profile extends Component {
7 | state = {
8 | userName: '',
9 | userInfo: []
10 |
11 | }
12 |
13 |
14 | async componentDidMount() {
15 | const userName = this.props.match.params.userName;
16 | const {data} = await getUser(userName);
17 | if (!data) return this.props.history.replace("/not-found");
18 | this.setState({userInfo: data, userName });
19 | }
20 |
21 |
22 | render() {
23 | const {userInfo} = this.state;
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
User Profile
31 |
60 |
61 |
62 |
63 |
64 |
65 | );
66 | }
67 | }
68 |
69 | export default Profile;
--------------------------------------------------------------------------------
/client/src/components/resetPassword.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Redirect} from "react-router-dom";
3 | import Joi from "joi-browser";
4 | import NotificationSystem from 'react-notification-system';
5 | import Form from "./common/form";
6 | import {sendLink} from '../services/userService';
7 | import auth from "../services/authService";
8 |
9 | class ResetPasswordForm extends Form{
10 | notificationSystem = React.createRef();
11 | state = {
12 | data: { username: ""},
13 | errors: {}
14 | }
15 |
16 | schema = {
17 | username: Joi.string()
18 | .required()
19 | .label("Username")
20 | };
21 |
22 | doSubmit = async () => {
23 | const notification = this.notificationSystem.current;
24 | try{
25 | const {data} = this.state;
26 | await sendLink(data);
27 | notification.addNotification({
28 | message: 'We have sent a rest password link to your email address!',
29 | level: 'success'
30 | });
31 | }catch(ex){
32 | if(ex.response && ex.response.status === 500){
33 | notification.addNotification({
34 | message: ex.response.data,
35 | level: 'error'
36 | });
37 | }
38 | }
39 | };
40 |
41 | render(){
42 | if(auth.getUser()) return
43 | return (
44 |
54 | );
55 | }
56 | }
57 |
58 | export default ResetPasswordForm;
--------------------------------------------------------------------------------
/client/src/components/searchBox.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 |
4 | const SearchBox = ({ value, onChange }) => {
5 | return (
6 |
7 | onChange(e.currentTarget.value)}
14 | />
15 |
16 | );
17 | };
18 |
19 | export default SearchBox;
20 |
--------------------------------------------------------------------------------
/client/src/components/signUpForm.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Joi from "joi-browser";
3 | import { Redirect } from 'react-router-dom';
4 | import NotificationSystem from 'react-notification-system';
5 | import Form from "./common/form";
6 | import { register } from "../services/userService";
7 | import auth from "../services/authService";
8 | import "../App.css";
9 |
10 | class RegisterForm extends Form {
11 | notificationSystem = React.createRef();
12 | state = {
13 | data: { email: "", password: "", name: "", username: ""},
14 | errors: {}
15 | };
16 |
17 | schema = {
18 | email: Joi.string()
19 | .required()
20 | .email()
21 | .label("Email"),
22 | password: Joi.string()
23 | .required()
24 | .min(5)
25 | .label("Password"),
26 | name: Joi.string()
27 | .required()
28 | .label("Name"),
29 | username: Joi.string()
30 | .required()
31 | .min(5)
32 | .label("Username")
33 | };
34 |
35 | cdm
36 |
37 | doSubmit = async () => {
38 | const notification = this.notificationSystem.current;
39 | const query = this.props.location.search;
40 | const params = new URLSearchParams(query);
41 | try{
42 | await register(this.state.data, params.get('ref'));
43 | notification.addNotification({
44 | message: 'Registered Successfully!',
45 | level: 'success'
46 | });
47 |
48 | window.setTimeout(()=>{
49 | window.location = '/login';
50 | },2000)
51 |
52 | }catch(ex){
53 | if(ex.response && ex.response.status === 400){
54 | notification.addNotification({
55 | message: ex.response.data,
56 | level: 'error'
57 | });
58 | }
59 | }
60 | };
61 |
62 | render() {
63 | if(auth.getUser()) return
64 | return (
65 |
78 | );
79 | }
80 | }
81 |
82 | export default RegisterForm;
83 |
--------------------------------------------------------------------------------
/client/src/components/users.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import _ from 'lodash';
3 | import NotificationSystem from 'react-notification-system';
4 | import Pagination from "./common/pagination";
5 | import { paginate } from "../utils/paginate";
6 | import Table from "./common/table";
7 | import {getUsers, deleteUser} from '../services/userService';
8 | import auth from '../services/authService';
9 |
10 | class Users extends Component {
11 | notificationSystem = React.createRef();
12 |
13 | columns = [
14 | {
15 | path: "name",
16 | label: "Name"
17 | },
18 | {
19 | path: "email",
20 | label: "Email"
21 | },
22 | {
23 | path: "username",
24 | label: "Username"
25 | },
26 | {
27 | path: "role",
28 | label: "Role"
29 | },
30 | {
31 | key: "delete",
32 | content: user => (
33 |
39 | )
40 | }
41 | ]
42 |
43 | state = {
44 | users : [],
45 | currentPage: 1,
46 | pageSize: 7,
47 | sortColumn: { path: "name", order: "asc" }
48 | }
49 |
50 | handleDelete = async user => {
51 | const notification = this.notificationSystem.current;
52 | if(user.role == 'ADMIN'){
53 | notification.addNotification({
54 | message: 'Admin can not be deleted!',
55 | level: 'error'
56 | });
57 | return;
58 | }
59 | const users = this.state.users.filter(m => m._id !== user._id);
60 | this.setState({ users });
61 | try{
62 | await deleteUser(user._id);
63 | notification.addNotification({
64 | message: 'Deleted Successfully',
65 | level: 'success'
66 | });
67 | }catch(ex){
68 | if(ex.response && ex.response.status === 400){
69 | notification.addNotification({
70 | message: ex.response.data,
71 | level: 'error'
72 | });
73 | }
74 | };
75 | }
76 |
77 | handleSort = sortColumn => {
78 | this.setState({ sortColumn });
79 | };
80 |
81 | getSortedData = ()=>{
82 | const {sortColumn} = this.state;
83 | const sorted = _.orderBy(this.state.users, [sortColumn.path], [sortColumn.order]);
84 | return sorted;
85 | }
86 |
87 | async componentDidMount(){
88 | const {data} = await getUsers();
89 | this.setState({users: data});
90 | }
91 |
92 | handlePageChange = page => {
93 | this.setState({ currentPage: page });
94 | };
95 |
96 | getPagedData = () => {
97 | const {
98 | pageSize,
99 | currentPage,
100 | sortColumn,
101 | users: allUsers
102 | } = this.state;
103 |
104 | const sorted = _.orderBy(allUsers, [sortColumn.path], [sortColumn.order]);
105 |
106 | const users = paginate(sorted, currentPage, pageSize);
107 |
108 | return { data: users, totalCount: allUsers.length };
109 | };
110 |
111 |
112 | render() {
113 | const {data, totalCount} = this.getPagedData();
114 | const {pageSize, currentPage} = this.state;
115 | return (
116 |
132 | );
133 | }
134 | }
135 |
136 | export default Users;
--------------------------------------------------------------------------------
/client/src/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiEndpoint": "http://localhost:3001/api",
3 | "thirdParty": "https://my-json-server.typicode.com/bnematzadeh/third-party-api/users"
4 | }
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0 0 0 0;
4 | font-family: sans-serif;
5 | background-image: url("public/back.jpg");
6 | background-repeat: no-repeat;
7 | background-attachment: fixed;
8 | background-size: cover;
9 | }
10 |
11 | .clickable {
12 | cursor: pointer;
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { BrowserRouter } from "react-router-dom";
4 | import "./index.css";
5 | import App from "./App";
6 | import registerServiceWorker from "./registerServiceWorker";
7 | import "bootstrap/dist/css/bootstrap.css";
8 | import "font-awesome/css/font-awesome.css";
9 |
10 | ReactDOM.render(
11 |
12 |
13 | ,
14 | document.getElementById("root")
15 | );
16 | registerServiceWorker();
17 |
--------------------------------------------------------------------------------
/client/src/public/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bnematzadeh/vulnerable-rest-api/33467261f3ec6a2b0aa9b9ff08c30a39a5572b9b/client/src/public/404.png
--------------------------------------------------------------------------------
/client/src/public/back.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bnematzadeh/vulnerable-rest-api/33467261f3ec6a2b0aa9b9ff08c30a39a5572b9b/client/src/public/back.jpg
--------------------------------------------------------------------------------
/client/src/public/logo-home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bnematzadeh/vulnerable-rest-api/33467261f3ec6a2b0aa9b9ff08c30a39a5572b9b/client/src/public/logo-home.png
--------------------------------------------------------------------------------
/client/src/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bnematzadeh/vulnerable-rest-api/33467261f3ec6a2b0aa9b9ff08c30a39a5572b9b/client/src/public/logo.png
--------------------------------------------------------------------------------
/client/src/public/owasp-brand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bnematzadeh/vulnerable-rest-api/33467261f3ec6a2b0aa9b9ff08c30a39a5572b9b/client/src/public/owasp-brand.png
--------------------------------------------------------------------------------
/client/src/public/owasp.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bnematzadeh/vulnerable-rest-api/33467261f3ec6a2b0aa9b9ff08c30a39a5572b9b/client/src/public/owasp.jpg
--------------------------------------------------------------------------------
/client/src/public/rest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bnematzadeh/vulnerable-rest-api/33467261f3ec6a2b0aa9b9ff08c30a39a5572b9b/client/src/public/rest.png
--------------------------------------------------------------------------------
/client/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/client/src/services/authService.js:
--------------------------------------------------------------------------------
1 | import http from './httpService';
2 | import api from '../config.json';
3 | import jwtDecode from 'jwt-decode';
4 |
5 | const token = "authToken";
6 |
7 | http.setJwt(getJwt());
8 |
9 | export async function login(username, password){
10 | const {headers} = await http.post(api.apiEndpoint + '/auth', {username, password});
11 | localStorage.setItem(token, headers['x-auth-token']);
12 | }
13 |
14 | export async function adminLogin(username, password){
15 | const {headers} = await http.post(api.apiEndpoint + '/adminAuth', {username, password});
16 | localStorage.setItem(token, headers['x-auth-token']);
17 | }
18 |
19 | export function logout(){
20 | localStorage.removeItem(token);
21 | }
22 |
23 | export function getUser(){
24 | try{
25 | const jwt = localStorage.getItem("authToken");
26 | return jwtDecode(jwt);
27 | }catch(ex){
28 | return null;
29 | }
30 | }
31 |
32 | export function getJwt(){
33 | return localStorage.getItem(token);
34 | }
35 |
36 | export default{
37 | login,
38 | adminLogin,
39 | logout,
40 | getUser,
41 | getJwt
42 | }
--------------------------------------------------------------------------------
/client/src/services/authorService.js:
--------------------------------------------------------------------------------
1 | import http from './httpService';
2 | import api from '../config.json';
3 |
4 | export function getAuthors() {
5 | return http.get(api.apiEndpoint + "/authors");
6 | }
7 |
8 | export function getAuthor(id){
9 | return http.get(api.apiEndpoint + "/authors/" + id);
10 | }
11 |
12 | export function addAuthor(author){
13 | return http.post(api.apiEndpoint + "/authors", author);
14 | }
15 |
16 | export function updateAuthor(authorId, author){
17 | return http.put(api.apiEndpoint + "/authors/" + authorId, author);
18 | }
19 |
20 | export function deleteAuthor(id){
21 | return http.delete(api.apiEndpoint + "/authors/" + id);
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/services/bookService.js:
--------------------------------------------------------------------------------
1 | import http from './httpService';
2 | import api from '../config.json';
3 |
4 | export function getBooks() {
5 | return http.get(api.apiEndpoint + "/books");
6 | }
7 |
8 | export function getBook(id){
9 | return http.get(api.apiEndpoint + "/books/" + id);
10 | }
11 |
12 |
13 | export function addBook(book){
14 | return http.post(api.apiEndpoint + "/books", book);
15 | }
16 |
17 | export function updateBook(bookId, book){
18 | return http.put(api.apiEndpoint + "/books/" + bookId, book );
19 | }
20 |
21 | export function deleteBook(id) {
22 | return http.delete(api.apiEndpoint + "/books/" + id);
23 | }
24 |
--------------------------------------------------------------------------------
/client/src/services/categoryService.js:
--------------------------------------------------------------------------------
1 | import http from './httpService';
2 | import api from '../config.json';
3 |
4 | export function getCategories() {
5 | return http.get(api.apiEndpoint + "/categories");
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/services/httpService.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | axios.interceptors.response.use(null, error =>{
4 | const expectedError =
5 | error.response &&
6 | error.response.status >= 400 &&
7 | error.response.status < 500;
8 |
9 | if(!expectedError){
10 | console.log("Logging the Error", error);
11 | }
12 |
13 | return Promise.reject(error);
14 |
15 | })
16 |
17 | function setJwt(jwt){
18 | axios.defaults.headers.common['x-auth-token'] = jwt;
19 | }
20 |
21 | export default {
22 | get: axios.get,
23 | post: axios.post,
24 | put: axios.put,
25 | delete: axios.delete,
26 | setJwt
27 | }
--------------------------------------------------------------------------------
/client/src/services/thirdParty.js:
--------------------------------------------------------------------------------
1 | import http from './httpService';
2 | import api from '../config.json';
3 |
4 | export function addSubscriber(email){
5 | return http.post(api.thirdParty, {
6 | email
7 | })
8 | }
--------------------------------------------------------------------------------
/client/src/services/userService.js:
--------------------------------------------------------------------------------
1 | import http from './httpService';
2 | import auth from './authService';
3 | import api from '../config.json';
4 |
5 | export function register(user, ref){
6 | return http.post(api.apiEndpoint + '/users',{
7 | name: user.name,
8 | email: user.email,
9 | username: user.username,
10 | password: user.password,
11 | ref: ref ? ref : ''
12 | })
13 | }
14 |
15 | export function getUsers(){
16 | return http.get(api.apiEndpoint + "/users");
17 | }
18 |
19 |
20 | export function getUser(userName){
21 | return http.get(api.apiEndpoint + "/users/" + userName);
22 | }
23 |
24 | export function updateUser(user){
25 | return http.put(api.apiEndpoint + "/users/" + auth.getUser()._id, {
26 | name: user.name,
27 | email: user.email,
28 | url: user.url,
29 | currentPass: user.currentPass,
30 | newPass: user.newPass
31 | });
32 | }
33 |
34 | export function deleteUser(userId){
35 | return http.delete(api.apiEndpoint + "/users/" + userId);
36 | }
37 |
38 | export function sendLink(user){
39 | return http.post(api.apiEndpoint + "/users/otp", user);
40 | }
41 |
42 | export function changePassword(value, user){
43 | console.log({password: value, user});
44 | return http.post(api.apiEndpoint + "/users/verify", {password: value, user});
45 | }
46 |
--------------------------------------------------------------------------------
/client/src/utils/paginate.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | export function paginate(items, pageNumber, pageSize) {
4 | const startIndex = (pageNumber - 1) * pageSize;
5 | return _(items)
6 | .slice(startIndex)
7 | .take(pageSize)
8 | .value();
9 | }
10 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 | services:
3 | client:
4 | depends_on:
5 | - api
6 | build: ./client
7 | ports:
8 | - 3000:3000
9 | api:
10 | depends_on:
11 | - db
12 | build: ./server
13 | ports:
14 | - 3001:3001
15 | environment:
16 | DB_URL: mongodb://db/vulnerable-rest-api
17 | env_file:
18 | - ./server/smtp_config.list
19 | command: ["sh", "docker-entrypoint.sh"]
20 | db:
21 | image: mongo:6.0
22 | ports:
23 | - 27017:27017
24 | volumes:
25 | - rest-api:/data/db
26 | volumes:
27 | rest-api:
28 |
--------------------------------------------------------------------------------
/server/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18.17.1-alpine3.18
2 |
3 | WORKDIR /app
4 | COPY package*.json ./
5 | RUN npm install
6 | COPY . .
7 |
8 | EXPOSE 3001
9 |
10 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/server/cert/private_key.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIICWwIBAAKBgQCOF3nBmg6RvafLYWK/wFj1fGr5+zrEduHAISOvRj6ooONxG1jT
3 | 9oYlqBdMWW9O90Icu3+FTVZAJI1h6TgXyHNYh/RD/zPp/c3jdcqRQxoVxi6pETuN
4 | 7XZJLmt2Z08z445MXReiRwVqBJ6ZTer15OKl95xv4UrcmKZ1J6IzOjTKrwIDAQAB
5 | AoGAMoReIVqoO2B2O3SQKyQzfHOThPSw3BB4Clq92R7cQA+TTS8v0Ywf/VtZjSdu
6 | z2BQ/O4cVEQ+medk7J6B9tKpTxJ+VCvMG5scXJLkxIRzYSoex6n+/bIB0eL1sxM5
7 | ch8AAz5IZVi9hDvof4/KRXzZW61ZluJmboahQxatRABvT0ECQQDlKj5797ZiK8ZF
8 | xZfN8R17COWevhpqgC1CBCqroUyY2/oODOySq7de8mOjIMeQx5dIGTcPlI04cTlc
9 | /oK5Yu5nAkEAnrsBfW5PN1fYMlV+m7Q5laQNRa1nBOwiSZQwV9KV8C7kD4pL3TDn
10 | JmPJ0Ho5gElfkKWWTDnhtm/uDB5ofzOEeQJABFmU7g15rMtf7BsgEoytHpTApw68
11 | uaDNOx+RH8jkB1LFoFamdvJDfM3lDkelAh6HxJqV7PnI5HVhvV4nZRV3qQI/dwvK
12 | EkipINjBTP7R9fU1OwXO7nMe0JJJJOX/W96UlY6VfRI706jZs/Ejazg/5/cftVx7
13 | XLAMmMdZwb0MoloBAkEArjVHP9F99CsK9/npoq2G6XTOyMZOvU1XFOc72EVOVGKn
14 | aRPFz/Vwe3TkrEoGZivNPkVCHdhVYDNEI8oHHMw0Gg==
15 | -----END RSA PRIVATE KEY-----
--------------------------------------------------------------------------------
/server/cert/pub_key.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCOF3nBmg6RvafLYWK/wFj1fGr5
3 | +zrEduHAISOvRj6ooONxG1jT9oYlqBdMWW9O90Icu3+FTVZAJI1h6TgXyHNYh/RD
4 | /zPp/c3jdcqRQxoVxi6pETuN7XZJLmt2Z08z445MXReiRwVqBJ6ZTer15OKl95xv
5 | 4UrcmKZ1J6IzOjTKrwIDAQAB
6 | -----END PUBLIC KEY-----
--------------------------------------------------------------------------------
/server/config/custom-environment-variables.json:
--------------------------------------------------------------------------------
1 | {
2 | "smtp_username": "smtp_username",
3 | "smtp_password": "smtp_password",
4 | "smtp_provider": "smtp_provider",
5 | "smtp_port": "smtp_port"
6 | }
--------------------------------------------------------------------------------
/server/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "db":"mongodb://localhost/vulnerable-rest-api"
3 | }
--------------------------------------------------------------------------------
/server/config/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "db":"mongodb://localhost/vulnerable-rest-api-test"
3 | }
--------------------------------------------------------------------------------
/server/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | echo "Waiting for MongoDB to start..."
4 | ./wait-for db:27017
5 |
6 | echo "Migrating the database..."
7 | npm run db:up
8 |
9 | echo "Starting the server..."
10 | npm start
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const winston = require('winston');
2 | const path = require('path');
3 | const express = require('express');
4 | const app = express();
5 | const appDev = express();
6 |
7 | require('./startup/logging')();
8 | require('./startup/routes')(app, appDev);
9 | require('./startup/db')();
10 |
11 | const port = process.env.PORT || 3001;
12 | const server = app.listen(port, () => { winston.info(`Listening on port ${port}`) });
13 |
14 | module.exports = server;
--------------------------------------------------------------------------------
/server/logfile.log:
--------------------------------------------------------------------------------
1 | {"message":"Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error","stack":"Error: Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error\n at Module._extensions..node (node:internal/modules/cjs/loader:1340:18)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)\n at Module.require (node:internal/modules/cjs/loader:1143:19)\n at require (node:internal/modules/cjs/helpers:121:18)\n at Object. (/app/node_modules/bcrypt/bcrypt.js:6:16)\n at Module._compile (node:internal/modules/cjs/loader:1256:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)","code":"ERR_DLOPEN_FAILED","level":"error","timestamp":"2023-08-20T13:47:03.134Z"}
2 | {"message":"Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error","stack":"Error: Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error\n at Module._extensions..node (node:internal/modules/cjs/loader:1340:18)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)\n at Module.require (node:internal/modules/cjs/loader:1143:19)\n at require (node:internal/modules/cjs/helpers:121:18)\n at Object. (/app/node_modules/bcrypt/bcrypt.js:6:16)\n at Module._compile (node:internal/modules/cjs/loader:1256:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)","code":"ERR_DLOPEN_FAILED","level":"error","timestamp":"2023-08-20T14:29:10.460Z"}
3 | {"message":"Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error","stack":"Error: Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error\n at Module._extensions..node (node:internal/modules/cjs/loader:1340:18)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)\n at Module.require (node:internal/modules/cjs/loader:1143:19)\n at require (node:internal/modules/cjs/helpers:121:18)\n at Object. (/app/node_modules/bcrypt/bcrypt.js:6:16)\n at Module._compile (node:internal/modules/cjs/loader:1256:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)","code":"ERR_DLOPEN_FAILED","level":"error","timestamp":"2023-08-20T14:46:50.130Z"}
4 | {"message":"Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error","stack":"Error: Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error\n at Module._extensions..node (node:internal/modules/cjs/loader:1340:18)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)\n at Module.require (node:internal/modules/cjs/loader:1143:19)\n at require (node:internal/modules/cjs/helpers:121:18)\n at Object. (/app/node_modules/bcrypt/bcrypt.js:6:16)\n at Module._compile (node:internal/modules/cjs/loader:1256:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)","code":"ERR_DLOPEN_FAILED","level":"error","timestamp":"2023-08-20T14:48:52.022Z"}
5 | {"message":"Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error","stack":"Error: Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error\n at Module._extensions..node (node:internal/modules/cjs/loader:1340:18)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)\n at Module.require (node:internal/modules/cjs/loader:1143:19)\n at require (node:internal/modules/cjs/helpers:121:18)\n at Object. (/app/node_modules/bcrypt/bcrypt.js:6:16)\n at Module._compile (node:internal/modules/cjs/loader:1256:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)","code":"ERR_DLOPEN_FAILED","level":"error","timestamp":"2023-08-20T15:11:12.811Z"}
6 | {"message":"Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error","stack":"Error: Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error\n at Module._extensions..node (node:internal/modules/cjs/loader:1340:18)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)\n at Module.require (node:internal/modules/cjs/loader:1143:19)\n at require (node:internal/modules/cjs/helpers:121:18)\n at Object. (/app/node_modules/bcrypt/bcrypt.js:6:16)\n at Module._compile (node:internal/modules/cjs/loader:1256:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)","code":"ERR_DLOPEN_FAILED","level":"error","timestamp":"2023-08-20T15:13:44.834Z"}
7 | {"message":"Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error","stack":"Error: Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error\n at Module._extensions..node (node:internal/modules/cjs/loader:1340:18)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)\n at Module.require (node:internal/modules/cjs/loader:1143:19)\n at require (node:internal/modules/cjs/helpers:121:18)\n at Object. (/app/node_modules/bcrypt/bcrypt.js:6:16)\n at Module._compile (node:internal/modules/cjs/loader:1256:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)","code":"ERR_DLOPEN_FAILED","level":"error","timestamp":"2023-08-20T15:19:23.207Z"}
8 | {"level":"info","message":"Listening on port 3001","timestamp":"2023-08-22T09:17:16.680Z"}
9 | {"level":"info","message":"Connected to mongodb://localhost/vul-rest-api","timestamp":"2023-08-22T09:17:16.698Z"}
10 | {"level":"info","message":"Listening on port 3001","timestamp":"2023-08-22T09:18:03.973Z"}
11 | {"level":"info","message":"Connected to mongodb://localhost/vulnerable-rest-api","timestamp":"2023-08-22T09:18:03.993Z"}
12 | {"level":"info","message":"Listening on port 3001","timestamp":"2023-08-22T09:28:51.711Z"}
13 | {"level":"info","message":"Connected to mongodb://localhost/vulnerable-rest-api","timestamp":"2023-08-22T09:28:51.722Z"}
14 | {"level":"info","message":"Listening on port 3001","timestamp":"2023-08-22T09:58:14.736Z"}
15 | {"level":"info","message":"Connected to mongodb://localhost/vulnerable-rest-api","timestamp":"2023-08-22T09:58:14.747Z"}
16 | {"message":"Configuration property \"smtp_provider\" is not defined","stack":"Error: Configuration property \"smtp_provider\" is not defined\n at Config.get (D:\\Vulnerable REST API\\server\\node_modules\\config\\lib\\config.js:179:11)\n at sendEmail (D:\\Vulnerable REST API\\server\\utils\\sendEmail.js:7:22)\n at D:\\Vulnerable REST API\\server\\routes\\users.js:88:5\n at processTicksAndRejections (internal/process/task_queues.js:95:5)","level":"error","timestamp":"2023-08-22T09:58:36.144Z"}
17 | {"level":"info","message":"Listening on port 3001","timestamp":"2023-08-22T09:59:41.317Z"}
18 | {"level":"info","message":"Connected to mongodb://localhost/vulnerable-rest-api","timestamp":"2023-08-22T09:59:41.326Z"}
19 | {"message":"Configuration property \"smtp_provider\" is not defined","stack":"Error: Configuration property \"smtp_provider\" is not defined\n at Config.get (D:\\Vulnerable REST API\\server\\node_modules\\config\\lib\\config.js:179:11)\n at sendEmail (D:\\Vulnerable REST API\\server\\utils\\sendEmail.js:7:22)\n at D:\\Vulnerable REST API\\server\\routes\\users.js:88:5\n at processTicksAndRejections (internal/process/task_queues.js:95:5)","level":"error","timestamp":"2023-08-22T09:59:48.804Z"}
20 | {"message":"Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error","stack":"Error: Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error\n at Module._extensions..node (node:internal/modules/cjs/loader:1340:18)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)\n at Module.require (node:internal/modules/cjs/loader:1143:19)\n at require (node:internal/modules/cjs/helpers:121:18)\n at Object. (/app/node_modules/bcrypt/bcrypt.js:6:16)\n at Module._compile (node:internal/modules/cjs/loader:1256:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)\n at Module.load (node:internal/modules/cjs/loader:1119:32)\n at Module._load (node:internal/modules/cjs/loader:960:12)","code":"ERR_DLOPEN_FAILED","level":"error","timestamp":"2023-08-25T16:32:36.659Z"}
21 | {"level":"info","message":"Listening on port 3001","timestamp":"2023-08-25T16:36:59.255Z"}
22 | {"level":"info","message":"Connected to mongodb://db/vulnerable-rest-api","timestamp":"2023-08-25T16:36:59.275Z"}
23 |
--------------------------------------------------------------------------------
/server/middleware/admin.js:
--------------------------------------------------------------------------------
1 | module.exports = function(req, res, next){
2 | if(req.user.payload.role !== 'ADMIN') return res.status(403).send('Access Forbidden!');
3 |
4 | next();
5 | }
--------------------------------------------------------------------------------
/server/middleware/auth.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const fs = require('fs');
3 |
4 | function auth(req, res, next){
5 | const token = req.header('X-Auth-Token');
6 | if(!token) return res.status(401).send({error: 'JWT token is not provided'});
7 |
8 | const publicKey = fs.readFileSync(`${__dirname}/../cert/pub_key.crt`);
9 | try{
10 | const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256', 'HS256'], complete: true});
11 | req.user = decoded;
12 | next();
13 | }catch(ex){
14 | res.status(400).send({error: 'Invalid token'});
15 | }
16 | }
17 |
18 | module.exports = auth;
--------------------------------------------------------------------------------
/server/middleware/cache.js:
--------------------------------------------------------------------------------
1 | const winston = require('winston');
2 | const nodecache = require('node-cache');
3 | const cache = new nodecache({stdTTL: 30});
4 |
5 | function cacheRoute(req, res, next) {
6 | const key = req.originalUrl;
7 | if((new RegExp('.*\.(css|js|png)$')).test(key)){
8 | if(cache.has(key)){
9 | console.log(cache.has(key))
10 | return res.set('Content-Type','application/json').send(JSON.parse(cache.get(key)));
11 | }else{
12 | res.sendResponse = res.send;
13 | res.send = (body) => {
14 | cache.set(key, JSON.stringify(body))
15 | res.sendResponse(body);
16 | }
17 | next();
18 | }
19 | }else{
20 | next();
21 | }
22 |
23 | }
24 |
25 | module.exports = cacheRoute;
--------------------------------------------------------------------------------
/server/middleware/error.js:
--------------------------------------------------------------------------------
1 | const winston = require('winston');
2 |
3 | module.exports = function(err, req, res, next){
4 | winston.error(err.message, err);
5 | res.status(500).send('Server Error');
6 | }
--------------------------------------------------------------------------------
/server/middleware/validateObjectId.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | module.exports = function(req,res,next){
4 | if(!mongoose.Types.ObjectId.isValid(req.params.id))
5 | return res.status(404).send('ObjectID is not valid');
6 | next();
7 | }
8 |
--------------------------------------------------------------------------------
/server/migrate-mongo-config.js:
--------------------------------------------------------------------------------
1 | // In this file you can configure migrate-mongo
2 | const dbConfig = require('config');
3 | const config = {
4 | mongodb: {
5 | // TODO Change (or review) the url to your MongoDB:
6 | url: process.env.DB_URL || dbConfig.get('db'),
7 |
8 | // TODO Change this to your database name:
9 | databaseName: "vulnerable-rest-api",
10 |
11 | options: {
12 | useNewUrlParser: true, // removes a deprecation warning when connecting
13 | useUnifiedTopology: true, // removes a deprecating warning when connecting
14 | // connectTimeoutMS: 3600000, // increase connection timeout to 1 hour
15 | // socketTimeoutMS: 3600000, // increase socket timeout to 1 hour
16 | }
17 | },
18 |
19 | // The migrations dir, can be an relative or absolute path. Only edit this when really necessary.
20 | migrationsDir: "migrations",
21 |
22 | // The mongodb collection where the applied changes are stored. Only edit this when really necessary.
23 | changelogCollectionName: "changelog",
24 |
25 | // The file extension to create migrations and search for in migration dir
26 | migrationFileExtension: ".js",
27 |
28 | // Enable the algorithm to create a checksum of the file contents and use that in the comparison to determine
29 | // if the file should be run. Requires that scripts are coded to be run multiple times.
30 | useFileHash: false,
31 |
32 | // Don't change this, unless you know what you're doing
33 | moduleSystem: 'commonjs',
34 | };
35 |
36 | module.exports = config;
37 |
--------------------------------------------------------------------------------
/server/migrations/20230820084942-authors.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | var ObjectId = mongoose.Types.ObjectId;
3 |
4 | module.exports = {
5 | async up(db, client) {
6 | await db
7 | .collection("authors")
8 | .insertMany([{
9 | "_id": new ObjectId("647e13103c18faca7f185cbc")
10 | ,
11 | "name": "Robert Sedgewick",
12 | "email": "robert.s@gmail.com",
13 | "phoneNumber": "+1-202-427-0140",
14 | "about": "Robert is a well-versed administrative assistant and office manager with five years of experience providing tailored support. Helping others and sharing knowledge are two beliefs Andrew imparts every day. His experience, positive attitude and willingness to help others allow him to excel in administrative roles.",
15 | "job": "Software Engineer"
16 | },{
17 | "_id": new ObjectId("647e13103c18faca7f185cbd")
18 | ,
19 | "name": "Tyler Martin",
20 | "email": "tyler@gmail.com",
21 | "phoneNumber": "+1-201-420-0123",
22 | "about": "Hi, my name is Tyler, and I believe that educating people about how culture and food correlate helps individuals understand more about themselves. I have nine years of experience exploring and discovering the unique recipes made by communities around the world, and I use my knowledge to create custom and memorable events. I believe that bringing together culture, food and people can help individuals connect and bond, and I intend to bring that experience to you.",
23 | "job": "Software Engineer"
24 | },{
25 | "_id": new ObjectId("647e13103c18faca7f185cbe")
26 | ,
27 | "name": "Erich Gamma",
28 | "email": "erichg@outlook.com",
29 | "phoneNumber": "+1-207-540-0123",
30 | "about": "Hello, I'm Erich. I'm an innovative and dedicated interior design professional dedicated to satisfying my customers' design requirements. I enjoy the challenge of finding unique ways to fulfill my customers' needs.",
31 | "job": "Designer"
32 | },{
33 | "_id": new ObjectId("647e13103c18faca7f185cbf")
34 | ,
35 | "name": "Borna Nematzadeh",
36 | "email": "borna.nematzadeh123@gmail.com",
37 | "phoneNumber": "+1-202-555-0123",
38 | "about": "I am a hard working, honest individual. I am a good timekeeper, always willing to learn new skills. I am friendly, helpful and polite, have a good sense of humour. I am able to work independently in busy environments and also within a team setting. I am outgoing and tactful, and able to listen effectively when solving problems.",
39 | "job": "Security Researcher"
40 | },{
41 | "_id": new ObjectId("647e13103c18faca7f185cc0")
42 | ,
43 | "name": "Angela Krakauer",
44 | "email": "angela.krakauer@gmail.com",
45 | "phoneNumber": "+1-209-510-0123",
46 | "about": "Hello, my name is Angela, and I believe that focusing on your company's data security plan is essential to growing your company's business. With over 10 years of experience in information and data security, my knowledge and skills can help you create effective security strategies. My dedication to creating comprehensive data security plans can also help your company improve its data integrity and increase customer retention.",
47 | "job": "Data Scientist"
48 | },{
49 | "_id": new ObjectId("647e13103c18faca7f185cc1")
50 | ,
51 | "name": "James Martin",
52 | "email": "james.ma@gmail.com",
53 | "phoneNumber": "+1-702-444-0123",
54 | "about": "I am a dedicated, organized and methodical individual. I have good interpersonal skills, am an excellent team worker and am keen and very willing to learn and develop new skills. I am reliable and dependable and often seek new responsibilities within a wide range of employment areas. I have an active and dynamic approach to work and getting things done. I am determined and decisive. I identify and develop opportunities.",
55 | "job": "Team Manager"
56 | },{
57 | "_id": new ObjectId("647e13103c18faca7f185cc2")
58 | ,
59 | "name": "George Demirov",
60 | "email": "georgede1@yahoo.com",
61 | "phoneNumber": "+1-304-210-0123",
62 | "about": "George is a versatile web designer due to his extensive history in graphic and web design. His dedication to continuously learning about new web design trends and concepts has made him a valuable member of the team. With eight years of experience and a master's in web design, his expertise can help customers modernize with websites and appeal to expanding customer populations.",
63 | "job": "Web Designer"
64 | }]);
65 | },
66 |
67 | async down(db, client) {
68 | await db.collection("authors").deleteMany({})
69 | }
70 | };
71 |
--------------------------------------------------------------------------------
/server/migrations/20230820161341-books.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | var ObjectId = mongoose.Types.ObjectId;
3 |
4 | module.exports = {
5 | async up(db, client) {
6 | await db
7 | .collection("books")
8 | .insertMany([{
9 | "_id":
10 | new ObjectId("647f2c9311da4d2da202dd77")
11 | ,
12 | "title": "Clean Code",
13 | "author":
14 | new ObjectId("647e13103c18faca7f185cc0")
15 | ,
16 | "category":
17 | new ObjectId("647f04587957d0d366afa4a4")
18 | ,
19 | "publishedDate": "4 September. 2018"
20 | },{
21 | "_id": new ObjectId("647f2c9311da4d2da202dd78")
22 | ,
23 | "title": "Design Patterns",
24 | "author": new ObjectId("647e13103c18faca7f185cbe")
25 | ,
26 | "category": new ObjectId("647f04587957d0d366afa4a4")
27 | ,
28 | "publishedDate": "4 September. 2018"
29 | },{
30 | "_id": new ObjectId("647f2c9311da4d2da202dd79")
31 | ,
32 | "title": "Smart Contract Security",
33 | "author": new ObjectId("647e13103c18faca7f185cc1")
34 | ,
35 | "category": new ObjectId("647f04587957d0d366afa4a8")
36 | ,
37 | "publishedDate": "12 December. 2018"
38 | }]);
39 | },
40 |
41 | async down(db, client) {
42 | // TODO write the statements to rollback your migration (if possible)
43 | // Example:
44 | // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: false}});
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/server/migrations/20230820161349-categories.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | var ObjectId = mongoose.Types.ObjectId;
3 |
4 | module.exports = {
5 | async up(db, client) {
6 | await db
7 | .collection("categories")
8 | .insertMany([{
9 | "_id": new ObjectId("647f04587957d0d366afa4a4")
10 | ,
11 | "name": "Programming"
12 | },{
13 | "_id": new ObjectId("647f04587957d0d366afa4a5")
14 | ,
15 | "name": "AI"
16 | },{
17 | "_id": new ObjectId("647f04587957d0d366afa4a6")
18 | ,
19 | "name": "Network"
20 | },{
21 | "_id": new ObjectId("647f04587957d0d366afa4a7")
22 | ,
23 | "name": "IoT"
24 | },{
25 | "_id": new ObjectId("647f04587957d0d366afa4a8")
26 | ,
27 | "name": "Blockchain"
28 | },{
29 | "_id": new ObjectId("647f04587957d0d366afa4a9")
30 | ,
31 | "name": "Web Security"
32 | }]);
33 | },
34 |
35 | async down(db, client) {
36 | // TODO write the statements to rollback your migration (if possible)
37 | // Example:
38 | // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: false}});
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/server/migrations/20230820171849-users.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | async up(db, client) {
3 | await db
4 | .collection("users")
5 | .insertMany([{
6 | "name": "Nick",
7 | "username": "admin",
8 | "email": "admin@gmail.com",
9 | "password": "$2b$10$TsOxUC1qsmh5CbPv0OaEzeunm2HQCiwzdfMJz4G1l4D0.MG/L3Hji",
10 | "role": "ADMIN"
11 | }]);
12 | },
13 |
14 | async down(db, client) {
15 | // TODO write the statements to rollback your migration (if possible)
16 | // Example:
17 | // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: false}});
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/server/models/author.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Author = mongoose.model('Author', new mongoose.Schema({
4 | name: {
5 | type: String,
6 | required: true
7 | },
8 | email: {
9 | type: String,
10 | required: true
11 | },
12 | job:{
13 | type: String,
14 | required: true
15 | },
16 | about: {
17 | type: String,
18 | minlength: 5,
19 | maxlength: 100,
20 | required: true
21 | },
22 | phoneNumber: {
23 | type: String
24 | }
25 | }));
26 |
27 | module.exports.Author = Author;
28 |
--------------------------------------------------------------------------------
/server/models/book.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Book = mongoose.model('Book', mongoose.Schema({
4 | title: {
5 | type: String,
6 | required: true
7 | },
8 | author:{
9 | type: mongoose.Schema.Types.ObjectId,
10 | ref: 'Author',
11 | required: true
12 | },
13 | category: {
14 | type: mongoose.Schema.Types.ObjectId,
15 | ref: 'Category',
16 | required: true
17 | },
18 | publishedDate: {
19 | type: String,
20 | required: true
21 | }
22 | }));
23 |
24 | module.exports.Book = Book;
--------------------------------------------------------------------------------
/server/models/category.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Category = mongoose.model('Category', new mongoose.Schema({
4 | name: {
5 | type: String,
6 | required: true
7 | }
8 | }));
9 |
10 | module.exports.Category = Category;
11 |
--------------------------------------------------------------------------------
/server/models/token.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const tokenSchema = new mongoose.Schema({
4 | userId: {
5 | type: mongoose.Schema.Types.ObjectId,
6 | required: true,
7 | ref: 'User',
8 | },
9 | token: {
10 | type: String,
11 | required: true,
12 | },
13 | createdAt: {
14 | type: Date,
15 | default: Date.now,
16 | expires: 3600,
17 | },
18 | });
19 |
20 | const Token = mongoose.model("Token", tokenSchema);
21 | module.exports.Token = Token;
--------------------------------------------------------------------------------
/server/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const jwt = require('jsonwebtoken');
3 | const fs = require('fs');
4 |
5 | const userSchema = new mongoose.Schema({
6 | name: {
7 | type: String,
8 | required: true,
9 | },
10 | username: {
11 | type: String,
12 | required: true
13 | },
14 | email: {
15 | type: String,
16 | required: true,
17 | },
18 | password: {
19 | type: String,
20 | required: true
21 | },
22 | website: {
23 | type: String
24 | },
25 | credit: {
26 | type: Number,
27 | default: 0
28 | },
29 | role: {
30 | type: String,
31 | enum: ["ADMIN", "USER"],
32 | default: 'USER'
33 | }
34 | })
35 |
36 | userSchema.methods.generateToken = function(){
37 | const privateKey = fs.readFileSync(__dirname+'/../cert/private_key.key');
38 | const token = jwt.sign({_id: this._id, name: this.name, role: this.role, username: this.username, website: this.website, credit: this.credit}, privateKey, { algorithm: 'RS256'});
39 | return token;
40 | }
41 |
42 | const User = mongoose.model('User', userSchema);
43 |
44 | module.exports.User = User;
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rest-api",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "db:up": "migrate-mongo up",
8 | "start": "nodemon --ignore './tests' index.js",
9 | "test": "jest --forceExit --detectOpenHandles --watchAll --maxWorkers=1 --verbose"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "bcrypt": "^5.1.0",
16 | "config": "^3.3.9",
17 | "connect": "^3.7.0",
18 | "cors": "^2.8.5",
19 | "express": "^4.18.2",
20 | "express-async-errors": "^3.1.1",
21 | "express-cache-controller": "^1.1.0",
22 | "ioredis": "^5.3.2",
23 | "jsonwebtoken": "^8.5.1",
24 | "lodash": "^4.17.21",
25 | "migrate-mongo": "^10.0.0",
26 | "mongoose": "^7.2.2",
27 | "needle": "^3.2.0",
28 | "node-cache": "^5.1.2",
29 | "nodemailer": "^6.9.3",
30 | "supertest": "^6.3.3",
31 | "vhost": "^3.0.2",
32 | "winston": "^2.4.0"
33 | },
34 | "devDependencies": {
35 | "jest": "^29.6.2",
36 | "nodemon": "^3.0.1"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/server/routes/adminAuth.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {User} = require('../models/user');
4 | const fs = require('fs');
5 | const bcrypt = require('bcrypt');
6 | const jwt = require('jsonwebtoken');
7 |
8 | router.post('/', async (req, res)=>{
9 |
10 | let user = await User.findOne({username: req.body.username});
11 | if(!user) return res.status(400).send('Invalid username or password');
12 |
13 | if(user.role !== 'ADMIN') return res.status(403).send('Only admin users can access this area');
14 |
15 | const validPassword = await bcrypt.compare(req.body.password, user.password);
16 | if(!validPassword) return res.status(400).send('Invalid username or password');
17 |
18 |
19 | const privateKey = fs.readFileSync(__dirname+'/../cert/private_key.key');
20 |
21 | const token = jwt.sign({_id: user._id, name: user.name, role: user.role, username: user.username, website: user.website}, privateKey, { algorithm: 'RS256'});
22 | res.header('X-Auth-Token', token).status(200).send(user);
23 | })
24 |
25 | module.exports = router;
26 |
27 |
--------------------------------------------------------------------------------
/server/routes/auth.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {User} = require('../models/user');
4 | const bcrypt = require('bcrypt');
5 | const jwt = require('jsonwebtoken');
6 |
7 | router.post('/', async (req, res)=>{
8 |
9 | let user = await User.findOne({'username':req.body.username});
10 | if(!user) return res.status(400).send('Invalid Username or Password');
11 |
12 | const validPassword = await bcrypt.compare(req.body.password, user.password);
13 | if(!validPassword) return res.status(400).send({error: 'Invalid Username or Password',userId: user._id});
14 |
15 | const token = user.generateToken();
16 | res.header('X-Auth-Token', token).status(200).send(user);
17 | })
18 |
19 | module.exports = router;
20 |
21 |
--------------------------------------------------------------------------------
/server/routes/authors.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const {Author} = require('../models/author');
3 | const auth = require('../middleware/auth');
4 | const _ = require('lodash');
5 | const router = express.Router();
6 |
7 | router.get('/', async (req,res)=>{
8 | const authors = await Author.find();
9 | res.send(authors);
10 | })
11 |
12 | router.get('/:id', async (req,res)=>{
13 | const author = await Author.findById({_id: req.params.id});
14 | if(!author) return res.status(404).send("Author Not Found");
15 | res.send(author);
16 | })
17 |
18 | router.post('/', auth, async(req,res)=>{
19 | let author = await Author.findOne({email: req.body.email});
20 | if(author) return res.status(400).send('Author is Already Existed!');
21 |
22 | author = new Author(req.body);
23 | author.save();
24 | res.status(201).send(author);
25 | })
26 |
27 | router.put('/:id', auth, async(req,res)=>{
28 | await Author.findByIdAndUpdate({_id: req.params.id}, {
29 | $set: {
30 | name: req.body.name,
31 | email: req.body.email,
32 | about: req.body.about,
33 | job: req.body.job
34 | }
35 | })
36 |
37 | res.send('Updated Successfully');
38 | })
39 |
40 | router.delete('/:id', auth, async(req,res)=>{
41 | const author = await Author.findByIdAndRemove({_id: req.params.id});
42 | if(!author) return res.status(404).send('The author with the given ID was not found');
43 |
44 | res.send(author);
45 | })
46 |
47 | module.exports = router;
48 |
49 |
--------------------------------------------------------------------------------
/server/routes/books.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const auth = require('../middleware/auth');
3 | const {Book} = require('../models/book');
4 | const router = express.Router()
5 |
6 | router.get('/', async (req,res)=>{
7 | const books = await Book.find().populate('author','name email phoneNumber').populate('category','name -_id');
8 | res.send(books);
9 | })
10 |
11 | router.get('/:id', async(req,res)=>{
12 | const book = await Book.findById({_id: req.params.id}).populate('author', 'name email phoneNumber -_id').populate('category', 'name -_id');
13 | res.send(book);
14 | })
15 |
16 | router.post('/', auth, async(req,res)=>{
17 | const book = new Book(req.body);
18 | await book.save();
19 | res.status(201).send(book);
20 | })
21 |
22 | router.put('/:id', auth, async(req,res)=>{
23 | const book = await Book.findByIdAndUpdate({_id: req.params.id}, {
24 | $set: {
25 | title: req.body.title,
26 | category: req.body.category,
27 | publishedDate: req.body.publishedDate,
28 | author: req.body.author
29 | }
30 | }, {new: true})
31 |
32 | res.send(book);
33 | })
34 |
35 | router.delete('/:id', auth ,async(req,res)=>{
36 | const book = await Book.findByIdAndRemove(req.params.id);
37 | if(!book) return res.status(404).send("The book with the given ID was not found");
38 |
39 | res.send(book);
40 | })
41 |
42 | module.exports = router;
43 |
44 |
--------------------------------------------------------------------------------
/server/routes/categories.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const {Category} = require('../models/category');
3 | const auth = require('../middleware/auth');
4 | const router = express.Router();
5 |
6 | router.get('/', async (req,res)=>{
7 | const categories = await Category.find();
8 | res.send(categories);
9 | })
10 |
11 | module.exports = router;
12 |
13 |
--------------------------------------------------------------------------------
/server/routes/log.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router();
2 | const path = require('path');
3 |
4 | router.get('/', (req,res)=>{
5 | res.setHeader('Content-type', "application/octet-stream");
6 | res.setHeader('Content-disposition', 'attachment; filename=logfile.log');
7 | res.sendFile(path.resolve(__dirname+'/../logfile.log'));
8 | })
9 |
10 | module.exports = router;
--------------------------------------------------------------------------------
/server/routes/me.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {User} = require('../models/user');
4 | const auth = require('../middleware/auth');
5 | const cacheRoute = require('../middleware/cache');
6 |
7 |
8 | router.get(/.*/, cacheRoute, auth, async(req,res)=>{
9 | if(req.query.id){
10 | const user = await User.findOne({_id: JSON.parse(decodeURI(req.query.id))});
11 | return res.send({
12 | user
13 | });
14 | }
15 | const user = await User.findOne({username: req.user.payload.username});
16 | return res.send({
17 | user
18 | });
19 | })
20 |
21 | module.exports = router;
--------------------------------------------------------------------------------
/server/routes/system.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router();
2 | const path = require('path');
3 |
4 | router.get('/key', (req,res)=>{
5 | res.setHeader('Content-type', "application/octet-stream");
6 | res.setHeader('Content-disposition', 'attachment; filename=pub_key.key');
7 | res.sendFile(path.resolve(__dirname+'/../cert/pub_key.crt'));
8 | })
9 |
10 | module.exports = router;
--------------------------------------------------------------------------------
/server/routes/users.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const bcrypt = require('bcrypt');
4 | const needle = require('needle');
5 | const _ = require('lodash');
6 | const sendEmail = require('../utils/sendEmail');
7 | const auth = require('../middleware/auth');
8 | const validateObjectId = require('../middleware/validateObjectId');
9 | const admin = require('../middleware/admin');
10 | const {User} = require('../models/user');
11 | const {Token} = require('../models/token');
12 | const { default: mongoose } = require('mongoose');
13 |
14 | router.get('/' , [auth,admin], async (req,res)=>{
15 | const users = await User.find();
16 | res.send(users);
17 | })
18 |
19 | router.get('/:name', auth, async(req,res)=>{
20 | const regex = /([a-zA-Z0-9]+)+$/;
21 | if(regex.test(req.params.name)){
22 | const user = await User.findOne({'username':req.params.name});
23 | return res.send(_.pick(user, ['name', 'email', 'role', 'username', 'website', '_id', 'credit']));
24 | }
25 | res.status(400).send('Invalid Name');
26 | })
27 |
28 | router.post('/', async (req, res)=>{
29 |
30 | let user = await User.findOne({email: req.body.email});
31 | if(user) return res.status(400).send('Invalid email or password');
32 |
33 | user = new User(req.body);
34 |
35 | if(req.body.ref){
36 | await User.findOneAndUpdate({_id: req.body.ref}, { $inc: { credit: 1 } })
37 | }
38 |
39 | const salt = await bcrypt.genSalt(10);
40 | user.password = await bcrypt.hash(user.password, salt);
41 | await user.save();
42 |
43 | res.send(user);
44 | })
45 |
46 | router.put('/:id', [auth, validateObjectId], async(req, res)=>{
47 |
48 | let user = await User.findOne({_id: req.params.id});
49 |
50 | var domain;
51 | await needle('get', req.body.url)
52 | .then(function(resp) { domain = resp.body; })
53 | .catch(function(err) { return; })
54 |
55 | const salt = await bcrypt.genSalt(10);
56 | user.password = await bcrypt.hash(req.body.newPass, salt);
57 |
58 | await User.findOneAndUpdate({_id: req.params.id}, {
59 | $set: {
60 | name: req.body.name,
61 | email: req.body.email,
62 | website: req.body.url,
63 | password: user.password
64 | }
65 | })
66 | res.send({status: 'Updated',domain});
67 | })
68 |
69 | router.post('/otp', async(req,res)=>{
70 | const user = await User.findOne({username: req.body.username});
71 | if(!user.email) return res.status(404).send('User does not exist!');
72 |
73 | // generate the token
74 | const generatedOTP = Math.floor(Math.random() * 9000 + 1000);
75 |
76 | // save the token to db
77 | const link = new Token({
78 | userId: user._id,
79 | token: generatedOTP,
80 | createdAt: Date.now()
81 | })
82 |
83 | const host = req.hostname;
84 | const resetLink = `http://${host}:3000/change-password?token=${link.token}&userId=${link.userId}`;
85 |
86 | await link.save();
87 | // send email to user
88 | sendEmail(user.email, resetLink);
89 |
90 | res.send({status: 'created', user: user._id});
91 | })
92 |
93 | router.post('/verify', async(req,res)=>{
94 | const user = await Token.findOne({userId: req.body.user.userId}).sort({"createdAt": -1}).limit(1);
95 | if(!user) return res.status(401).send('Token has expired!');
96 |
97 | if(user.token !== req.body.user.token) return res.status(401).send('Access Denied!');
98 |
99 | const salt = await bcrypt.genSalt(10);
100 | const password = await bcrypt.hash(req.body.password.value, salt);
101 | await User.findOneAndUpdate({_id: user.userId}, {
102 | $set: {
103 | password
104 | }
105 | })
106 |
107 | await Token.findOneAndRemove({userId: req.body.user.userId});
108 | res.send('Updated Successfully!');
109 | })
110 |
111 | router.delete('/:id', [auth, validateObjectId], async(req,res)=>{
112 | const user = await User.findByIdAndRemove({_id: req.params.id});
113 |
114 | if(!user) return res.status(404).send('The user with the given ID was not found')
115 | res.send("Deleted Successfully!");
116 | })
117 |
118 | module.exports = router;
119 |
120 |
--------------------------------------------------------------------------------
/server/routes/usersDev.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const auth = require('../middleware/auth');
4 | const {User} = require('../models/user');
5 |
6 | router.get('/' , async (req,res)=>{
7 | const users = await User.find();
8 | res.send(users);
9 | })
10 |
11 | module.exports = router;
12 |
13 |
--------------------------------------------------------------------------------
/server/smtp_config.list:
--------------------------------------------------------------------------------
1 | smtp_username=USERNAME
2 | smtp_password=PASSWORD
3 | smtp_provider=SMTP PROVIDER
4 | smtp_port=PORT
--------------------------------------------------------------------------------
/server/startup/db.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const winston = require('winston');
3 | const config = require('config');
4 |
5 | module.exports = function(){
6 | const db = process.env.DB_URL || config.get('db');
7 | mongoose.connect(db)
8 | .then(()=>{winston.info(`Connected to ${db}`)});
9 | }
--------------------------------------------------------------------------------
/server/startup/logging.js:
--------------------------------------------------------------------------------
1 | require('express-async-errors');
2 | const winston = require('winston');
3 |
4 | module.exports = function(){
5 | winston.add(winston.transports.File, {filename: 'logfile.log'});
6 | process.on('uncaughtException', (ex)=>{
7 | winston.error(ex.message, ex);
8 | })
9 |
10 | process.on('unhandledRejection', (ex)=>{
11 | winston.error(ex.message, ex);
12 | })
13 | }
--------------------------------------------------------------------------------
/server/startup/routes.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const cors = require('cors');
3 | const vhost = require('vhost');
4 | const me = require('../routes/me');
5 | const authors = require('../routes/authors');
6 | const books = require('../routes/books');
7 | const categories = require('../routes/categories');
8 | const users = require('../routes/users');
9 | const usersDev = require('../routes/usersDev');
10 | const auth = require('../routes/auth');
11 | const adminAuth = require('../routes/adminAuth');
12 | const system = require('../routes/system');
13 | const log = require('../routes/log');
14 | const error = require('../middleware/error');
15 |
16 | const corsOptions = {
17 | exposedHeaders: 'X-Auth-Token',
18 | };
19 |
20 | module.exports = function(app, appDev){
21 | app.use(vhost('dev.localhost', appDev));
22 | app.use(cors(corsOptions));
23 | app.use(express.json());
24 | app.use('/api/authors', authors);
25 | app.use('/api/books', books);
26 | app.use('/api/categories', categories);
27 | app.use('/api/users', users);
28 | app.use('/api/auth', auth);
29 | app.use('/api/adminAuth', adminAuth);
30 | app.use('/api/system', system);
31 | app.use('/api/logs', log);
32 | app.use('/api/me', me);
33 | appDev.use('/api/users', usersDev);
34 | app.use(error);
35 | }
--------------------------------------------------------------------------------
/server/utils/sendEmail.js:
--------------------------------------------------------------------------------
1 | const nodemailer = require('nodemailer');
2 | const winston = require('winston');
3 | const config = require('config');
4 |
5 | module.exports = async function sendEmail(recipient_email, link){
6 | var smtpConfig = {
7 | host: config.get('smtp_provider'),
8 | port: config.get('smtp_port') || 2525,
9 | auth: {
10 | user: config.get('smtp_username'),
11 | pass: config.get('smtp_password')
12 | }
13 |
14 | };
15 |
16 | const transporter = nodemailer.createTransport(smtpConfig);
17 |
18 | const info = await transporter.sendMail({
19 | from: 'Vulnerable-Rest-API ',
20 | to: recipient_email,
21 | subject: 'Password Recovery',
22 | html: `
23 |
24 |
25 |
26 | OTP
27 |
28 |
29 |
30 |
31 |
32 |
35 |
Hi,
36 |
Use the following link to reset your password.
37 |
Reset Password
38 |
Regards,
Vulnerable REST API
39 |
40 |
41 |
42 |
43 |
44 |
45 | `
46 | })
47 |
48 | winston.info(info.messageId);
49 | }
50 |
--------------------------------------------------------------------------------
/server/wait-for:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # The MIT License (MIT)
4 | #
5 | # Copyright (c) 2017 Eficode Oy
6 | #
7 | # Permission is hereby granted, free of charge, to any person obtaining a copy
8 | # of this software and associated documentation files (the "Software"), to deal
9 | # in the Software without restriction, including without limitation the rights
10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | # copies of the Software, and to permit persons to whom the Software is
12 | # furnished to do so, subject to the following conditions:
13 | #
14 | # The above copyright notice and this permission notice shall be included in all
15 | # copies or substantial portions of the Software.
16 | #
17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | # SOFTWARE.
24 |
25 | set -- "$@" -- "$TIMEOUT" "$QUIET" "$HOST" "$PORT" "$result"
26 | TIMEOUT=15
27 | QUIET=0
28 |
29 | echoerr() {
30 | if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
31 | }
32 |
33 | usage() {
34 | exitcode="$1"
35 | cat << USAGE >&2
36 | Usage:
37 | $cmdname host:port [-t timeout] [-- command args]
38 | -q | --quiet Do not output any status messages
39 | -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout
40 | -- COMMAND ARGS Execute command with args after the test finishes
41 | USAGE
42 | exit "$exitcode"
43 | }
44 |
45 | wait_for() {
46 | if ! command -v nc >/dev/null; then
47 | echoerr 'nc command is missing!'
48 | exit 1
49 | fi
50 |
51 | while :; do
52 | nc -z "$HOST" "$PORT" > /dev/null 2>&1
53 |
54 | result=$?
55 | if [ $result -eq 0 ] ; then
56 | if [ $# -gt 6 ] ; then
57 | for result in $(seq $(($# - 6))); do
58 | result=$1
59 | shift
60 | set -- "$@" "$result"
61 | done
62 |
63 | TIMEOUT=$2 QUIET=$3 HOST=$4 PORT=$5 result=$6
64 | shift 6
65 | exec "$@"
66 | fi
67 | exit 0
68 | fi
69 |
70 | if [ "$TIMEOUT" -le 0 ]; then
71 | break
72 | fi
73 | TIMEOUT=$((TIMEOUT - 1))
74 |
75 | sleep 1
76 | done
77 | echo "Operation timed out" >&2
78 | exit 1
79 | }
80 |
81 | while :; do
82 | case "$1" in
83 | *:* )
84 | HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
85 | PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
86 | shift 1
87 | ;;
88 | -q | --quiet)
89 | QUIET=1
90 | shift 1
91 | ;;
92 | -q-*)
93 | QUIET=0
94 | echoerr "Unknown option: $1"
95 | usage 1
96 | ;;
97 | -q*)
98 | QUIET=1
99 | result=$1
100 | shift 1
101 | set -- -"${result#-q}" "$@"
102 | ;;
103 | -t | --timeout)
104 | TIMEOUT="$2"
105 | shift 2
106 | ;;
107 | -t*)
108 | TIMEOUT="${1#-t}"
109 | shift 1
110 | ;;
111 | --timeout=*)
112 | TIMEOUT="${1#*=}"
113 | shift 1
114 | ;;
115 | --)
116 | shift
117 | break
118 | ;;
119 | --help)
120 | usage 0
121 | ;;
122 | -*)
123 | QUIET=0
124 | echoerr "Unknown option: $1"
125 | usage 1
126 | ;;
127 | *)
128 | QUIET=0
129 | echoerr "Unknown argument: $1"
130 | usage 1
131 | ;;
132 | esac
133 | done
134 |
135 | if ! [ "$TIMEOUT" -ge 0 ] 2>/dev/null; then
136 | echoerr "Error: invalid timeout '$TIMEOUT'"
137 | usage 3
138 | fi
139 |
140 | if [ "$HOST" = "" -o "$PORT" = "" ]; then
141 | echoerr "Error: you need to provide a host and port to test."
142 | usage 2
143 | fi
144 |
145 | wait_for "$@"
146 |
--------------------------------------------------------------------------------
/vulnerabilities/vulnerabilities.md:
--------------------------------------------------------------------------------
1 | # OWASP API Security Top 10 - 2023
2 |
3 | # Broken Object Property Level Authorization
4 |
5 | - Mass Assignment
6 | - Vulnerable Endpoints
7 | - POST /api/users
8 | - Excessive Data Exposure
9 | - Vulnerable Endpoints
10 | - GET /api/authors
11 | - GET /api/books
12 |
13 | # Broken Object Level Authorization
14 |
15 | - Vulnerable Endpoints
16 | - GET /api/users/:name
17 | - PUT /api/users/:id
18 | - Get the id from the following endpoints
19 | - POST /api/auth (Add invalid password for a valid username and check the API response)
20 | - POST /api/otp (Add username and check the API response)
21 |
22 | # Broken Function Level Authorization
23 |
24 | - Vulnerable Endpoints
25 | - POST /api/books
26 | - PUT /api/books/:bookId
27 | - DELETE /api/books/:bookId
28 | - POST /api/authors
29 | - PUT /api/authors/:authorId
30 | - DELETE /api/authors/:authorId
31 |
32 | # Server-Side Request Forgery
33 |
34 | - Vulnerable Endpoint
35 | - PUT /api/users/:id
36 |
37 | # Improper Inventory Management
38 |
39 | - localhost:3001/api/users: Access Forbidden
40 | - dev.localhost:3001/api/users: 200 Ok
41 |
42 | # Unsafe Consumption of APIs
43 |
44 | The API Does not properly validate and sanitize data gathered from other APIs. The API sends the following request to store email in a third-party API.
45 |
46 | - POST https://my-json-server.typicode.com/bnematzadeh/third-party-api/users
47 |
48 | - Navigate to /about. Add your XSS payload in email and subscribe. The subscribed email will be returned from the third-party API to the application.
49 |
50 | - localhost:3000/about?email={payload}
51 |
52 | # Broken Authentication
53 |
54 | - JWT Key Confusion
55 |
56 | - GET /api/system/key: public key
57 |
58 | - Weak Password
59 | - Admin Login: http://localhost:3000/admin/login
60 | - Vulnerable Endpoint: POST /api/adminAuth
61 | - Credentials:
62 | - Username: admin
63 | - Password: admin1234
64 | - Weak Implementation of Reset Password (Account Takeover)
65 | - POST /api/users/verify
66 |
67 | # Security Misconfiguration
68 |
69 | - The logging is enabled in this application. Send a request to the following endpoint to access the log file
70 | - Vulnerable Endpoint
71 | - GET /api/logs
72 |
73 | # Unrestricted Resource Consumption
74 |
75 | - ReDOS
76 | - Vulnerable Endpoint
77 | - GET /api/users/:name
78 |
79 | # Unrestricted Access to Sensitive Business Flows
80 |
81 | Users can invite their friends and gain credit for each friend who has joined the app. This credit can be later used as cash to get a free book. An attacker exploits this flow by writing a script to automate the registration process, with each new user adding credit to the attacker's account.
82 |
83 | - Automation Process
84 | GET /profile/{YourUsername}: Get ref link from your profile
85 | POST /api/users + ref link
86 |
87 | # Bonus
88 |
89 | # Injection
90 |
91 | - NOSQL injection
92 | - Vulnerable Endpoint
93 | - GET /api/me?id={payload}
94 | - XSS
95 | - Vulnerable Endpoint
96 | - PUT /api/users/:id
97 |
98 | # Web Cache Deception
99 |
100 | - Vulnerable Endpoint: GET /api/me
101 |
--------------------------------------------------------------------------------