From 8bdc22b20621aad1cd571dffd0db79935e1f39f6 Mon Sep 17 00:00:00 2001 From: ws2016 Date: Wed, 1 Jul 2026 20:18:04 +0000 Subject: [PATCH] fix: address critical security issues in API specification - Add JWT Bearer authentication scheme (was completely missing) - Split UserModel into separate request/response schemas to prevent password leakage in API responses - Add authentication requirements to all sensitive endpoints (GetAvailableFunds, ProfileImage, PortalSearch) - Remove IDOR-susceptible user query params from own-data endpoints - Add input validation constraints (minLength, maxLength, minimum, maximum, pattern) to all request fields - Remove client-controlled price from BuyProductReq - Change Logout from GET to POST (CSRF prevention) - Add 429 rate limiting responses to auth endpoints - Fix user enumeration in PasswordRecovery - Add 403 Forbidden responses to admin endpoints - Move OpenAPI spec from README.md to openapi.json - Add SECURITY.md documenting all findings and fixes Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- README.md | 1226 +------------------------------------------- SECURITY.md | 81 +++ openapi.json | 1376 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1472 insertions(+), 1211 deletions(-) create mode 100644 SECURITY.md create mode 100644 openapi.json diff --git a/README.md b/README.md index 58efaa2..0a14bdf 100644 --- a/README.md +++ b/README.md @@ -1,1211 +1,15 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "BankWeb API", - "version": "v1" - }, - "paths": { - "/api/Admin/Transactions": { - "get": { - "tags": [ - "Admin" - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TransactionRespDataTableResp" - } - } - } - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/Admin/GetAllUsers": { - "get": { - "tags": [ - "Admin" - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminUserInfoRespDataTableResp" - } - } - } - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/AdminStore/GetAllPurchases": { - "get": { - "tags": [ - "AdminStore" - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PurcahseHistoryItemResp" - } - } - } - } - } - }, - "400": { - "description": "Bad Request" - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/AdminStore/GetStoreItems": { - "get": { - "tags": [ - "AdminStore" - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StoreItem" - } - } - } - } - }, - "400": { - "description": "Bad Request" - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/AdminStore/Create": { - "post": { - "tags": [ - "AdminStore" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreItem" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/StoreItem" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/StoreItem" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmptyResult" - } - } - } - }, - "400": { - "description": "Bad Request" - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/AdminStore/Get/{id}": { - "get": { - "tags": [ - "AdminStore" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreItem" - } - } - } - }, - "400": { - "description": "Bad Request" - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/AdminStore/Edit/{id}": { - "post": { - "tags": [ - "AdminStore" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreItem" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/StoreItem" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/StoreItem" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreItem" - } - } - } - }, - "400": { - "description": "Bad Request" - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/AdminStore/DeleteStoreItem": { - "post": { - "tags": [ - "AdminStore" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreItem" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/StoreItem" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/StoreItem" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmptyResult" - } - } - } - }, - "400": { - "description": "Bad Request" - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/Auth/Login": { - "post": { - "tags": [ - "Auth" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - } - } - }, - "400": { - "description": "Bad Request" - } - } - } - }, - "/api/Auth/Logout": { - "get": { - "tags": [ - "Auth" - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmptyResult" - } - } - } - } - } - } - }, - "/api/Auth/Register": { - "post": { - "tags": [ - "Auth" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmptyResult" - } - } - } - }, - "400": { - "description": "Bad Request" - } - } - } - }, - "/api/Auth/PasswordRecovery": { - "post": { - "tags": [ - "Auth" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmptyResult" - } - } - } - }, - "400": { - "description": "Bad Request" - } - } - } - }, - "/api/Auth/Recover": { - "post": { - "tags": [ - "Auth" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmptyResult" - } - } - } - }, - "400": { - "description": "Bad Request" - } - } - } - }, - "/api/ContactUs/ContactUs": { - "post": { - "tags": [ - "ContactUs" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UserModel" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmptyResult" - } - } - } - }, - "400": { - "description": "Bad Request" - } - } - } - }, - "/api/PortalSearch/Index": { - "get": { - "tags": [ - "PortalSearch" - ], - "parameters": [ - { - "name": "searchString", - "in": "query", - "schema": { - "type": "string", - "nullable": true - } - } - ], - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/api/Search/FindUser": { - "get": { - "tags": [ - "Search" - ], - "parameters": [ - { - "name": "term", - "in": "query", - "schema": { - "type": "string", - "nullable": true - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/Store/GetStoreItems": { - "get": { - "tags": [ - "Store" - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StoreItem" - } - } - } - } - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/Store/GetHistory": { - "get": { - "tags": [ - "Store" - ], - "parameters": [ - { - "name": "username", - "in": "query", - "schema": { - "type": "string", - "nullable": true - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PurcahseHistoryItemResp" - } - } - } - } - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/Store/BuyProduct": { - "post": { - "tags": [ - "Store" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BuyProductReq" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/BuyProductReq" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/BuyProductReq" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmptyResult" - } - } - } - }, - "400": { - "description": "Bad Request" - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/Transaction/Get/{id}": { - "get": { - "tags": [ - "Transaction" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TransactionDBModel" - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/api/Transaction/GetTransactions": { - "get": { - "tags": [ - "Transaction" - ], - "parameters": [ - { - "name": "start", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "length", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "search[value]", - "in": "query", - "schema": { - "type": "string", - "nullable": true - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TransactionRespDataTableResp" - } - } - } - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/Transaction/GetTransactionHistory": { - "get": { - "tags": [ - "Transaction" - ], - "parameters": [ - { - "name": "userName", - "in": "query", - "schema": { - "type": "string", - "nullable": true - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionResp" - } - } - } - } - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/Transaction/Create": { - "post": { - "tags": [ - "Transaction" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TransactionDBModel" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/TransactionDBModel" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/TransactionDBModel" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmptyResult" - } - } - } - }, - "400": { - "description": "Bad Request" - }, - "401": { - "description": "Unauthorized" - } - } - } - }, - "/api/User/ProfileImage": { - "get": { - "tags": [ - "User" - ], - "parameters": [ - { - "name": "user", - "in": "query", - "schema": { - "type": "string", - "nullable": true - } - } - ], - "responses": { - "200": { - "description": "Success" - } - } - }, - "post": { - "tags": [ - "User" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewImageModel" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/NewImageModel" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/NewImageModel" - } - } - } - }, - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/api/User/GetAvailableFunds": { - "get": { - "tags": [ - "User" - ], - "parameters": [ - { - "name": "user", - "in": "query", - "schema": { - "type": "string", - "nullable": true - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccountBalanceResp" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "TransactionResp": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "senderId": { - "type": "string", - "nullable": true - }, - "receiverId": { - "type": "string", - "nullable": true - }, - "dateTime": { - "type": "string", - "nullable": true - }, - "reason": { - "type": "string", - "nullable": true - }, - "amount": { - "type": "number", - "format": "double" - }, - "reference": { - "type": "string", - "nullable": true - }, - "senderName": { - "type": "string", - "nullable": true - }, - "senderSurname": { - "type": "string", - "nullable": true - }, - "receiverName": { - "type": "string", - "nullable": true - }, - "receiverSurname": { - "type": "string", - "nullable": true - } - } - }, - "TransactionRespDataTableResp": { - "type": "object", - "properties": { - "recordsTotal": { - "type": "integer", - "format": "int32" - }, - "recordsFiltered": { - "type": "integer", - "format": "int32" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionResp" - }, - "nullable": true - } - } - }, - "AdminUserInfoResp": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true - }, - "surname": { - "type": "string", - "nullable": true - }, - "username": { - "type": "string", - "nullable": true - }, - "role": { - "type": "integer", - "format": "int32" - } - } - }, - "AdminUserInfoRespDataTableResp": { - "type": "object", - "properties": { - "recordsTotal": { - "type": "integer", - "format": "int32" - }, - "recordsFiltered": { - "type": "integer", - "format": "int32" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AdminUserInfoResp" - }, - "nullable": true - } - } - }, - "PurcahseHistoryItemResp": { - "type": "object", - "properties": { - "purchaseTime": { - "type": "string", - "format": "date-time" - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "quantity": { - "type": "integer", - "format": "int32" - }, - "price": { - "type": "number", - "format": "double" - }, - "userName": { - "type": "string", - "nullable": true - } - } - }, - "StoreItem": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "price": { - "type": "number", - "format": "double" - }, - "installments": { - "type": "integer", - "format": "int32" - } - } - }, - "EmptyResult": { - "type": "object" - }, - "UserModel": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "userName": { - "type": "string", - "nullable": true - }, - "password": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "surname": { - "type": "string", - "nullable": true - }, - "userRight": { - "type": "integer", - "format": "int32" - }, - "accountNumber": { - "type": "string", - "nullable": true - }, - "cookie": { - "type": "string", - "nullable": true - }, - "status": { - "type": "string", - "nullable": true - }, - "siteAction": { - "type": "string", - "nullable": true - }, - "token": { - "type": "string", - "nullable": true - } - } - }, - "BuyProductReq": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "quantity": { - "type": "integer", - "format": "int32" - }, - "price": { - "type": "number", - "format": "double" - } - } - }, - "TransactionDBModel": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "senderId": { - "type": "string", - "nullable": true - }, - "receiverId": { - "type": "string", - "nullable": true - }, - "transactionDateTime": { - "type": "string", - "format": "date-time" - }, - "reason": { - "type": "string", - "nullable": true - }, - "amount": { - "type": "number", - "format": "double" - }, - "reference": { - "type": "string", - "nullable": true - } - } - }, - "NewImageModel": { - "type": "object", - "properties": { - "username": { - "type": "string", - "nullable": true - }, - "imageUrl": { - "type": "string", - "nullable": true - } - } - }, - "AccountBalanceResp": { - "type": "object", - "properties": { - "balance": { - "type": "number", - "format": "double" - } - } - } - } - } -} +# BankWeb API + +A banking web application API providing transaction management, user management, store operations, and admin functionality. + +## API Documentation + +The OpenAPI 3.0 specification for this API is available in [`openapi.json`](./openapi.json). + +## Security Notes + +- All authenticated endpoints require a valid Bearer (JWT) token in the `Authorization` header. +- Admin endpoints (`/api/Admin/*`, `/api/AdminStore/*`) require admin-level privileges. +- User passwords are never returned in API responses. +- Input validation constraints (length, format, range) are enforced on all request fields. +- CORS should be configured to allow only trusted origins in production. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..7438e51 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,81 @@ +# Security Audit Report + +## Issues Found + +### CRITICAL + +#### 1. No Authentication Scheme Defined +- **Location**: Root-level `security` and `components/securitySchemes` sections were completely absent. +- **Impact**: Without a defined security scheme, API consumers and code generators cannot enforce authentication. Any endpoint could be called without credentials. +- **Fix**: Added `BearerAuth` (JWT) security scheme in `components/securitySchemes` and applied it globally and per-endpoint. + +#### 2. Password Exposed in API Responses +- **Location**: `/api/Auth/Login` used `UserModel` (which includes `password`) as both request AND response schema. +- **Impact**: Passwords could be returned to clients in API responses, leading to credential exposure in logs, browser history, and network traces. +- **Fix**: Split into separate `LoginRequest` (accepts credentials) and `LoginResponse` (returns token + user info, never password). + +#### 3. Missing Authentication on Sensitive Endpoints +- **Location**: `/api/User/GetAvailableFunds`, `/api/User/ProfileImage`, `/api/PortalSearch/Index` +- **Impact**: Account balance and profile data accessible without authentication. +- **Fix**: Added `security: [{"BearerAuth": []}]` and `401` responses to all sensitive endpoints. + +### HIGH + +#### 4. IDOR (Insecure Direct Object Reference) Vulnerabilities +- **Location**: `/api/Store/GetHistory?username=`, `/api/Transaction/GetTransactionHistory?userName=`, `/api/User/GetAvailableFunds?user=`, `/api/User/ProfileImage?user=` +- **Impact**: Any authenticated user could pass another user's username and access their data (purchase history, transaction history, balance, profile image). +- **Fix**: Removed user-controlled query parameters for own-data endpoints. Server must derive the user identity from the JWT token. Added `403` responses and documentation noting IDOR prevention requirements. + +#### 5. No Input Validation Constraints +- **Location**: All request schemas (search strings, passwords, amounts, IDs) +- **Impact**: SQL injection via unvalidated search strings, buffer overflow via unbounded inputs, negative transaction amounts enabling fund theft, DoS via extremely large payloads. +- **Fix**: Added `minLength`, `maxLength`, `minimum`, `maximum`, and `pattern` constraints to all input fields. + +#### 6. Client-Controlled Price in Purchase Request +- **Location**: `BuyProductReq` schema contained a `price` field. +- **Impact**: Client could submit an arbitrary price (e.g., $0.01) to purchase items at a discount. +- **Fix**: Removed `price` from `BuyProductReq`. Price must be determined server-side from the product catalog. + +### MEDIUM + +#### 7. Logout Endpoint Used GET Method +- **Location**: `/api/Auth/Logout` was a GET request. +- **Impact**: Logout could be triggered via CSRF (e.g., ``) or by link prefetching. +- **Fix**: Changed to POST method. + +#### 8. No Rate Limiting Indicated +- **Location**: `/api/Auth/Login`, `/api/Auth/Register`, `/api/Auth/PasswordRecovery` +- **Impact**: Brute-force attacks on login, mass account creation, email bombing via password recovery. +- **Fix**: Added `429 Too Many Requests` response to authentication and public-facing endpoints. + +#### 9. User Enumeration via Password Recovery +- **Location**: `/api/Auth/PasswordRecovery` previously returned `400` for invalid users. +- **Impact**: Attackers can enumerate valid usernames by observing different responses. +- **Fix**: Documented that endpoint must always return `200` regardless of whether username exists. + +#### 10. Mixed Request/Response Models (Information Leakage) +- **Location**: `UserModel` schema contained sensitive fields (`cookie`, `token`, `password`, `userRight`, `accountNumber`) used in both directions. +- **Impact**: Internal implementation details and sensitive data could leak to clients. +- **Fix**: Created purpose-specific schemas: `LoginRequest`, `LoginResponse`, `RegisterRequest`, `PasswordRecoveryRequest`, `PasswordResetRequest`. + +### LOW + +#### 11. Missing `required` Markers on Request Bodies +- **Location**: All request body schemas. +- **Impact**: API accepts incomplete requests, leading to undefined server behavior. +- **Fix**: Added `required` arrays to all request schemas. + +#### 12. Profile Image URL Not Validated +- **Location**: `NewImageModel.imageUrl` had no format constraint. +- **Impact**: SSRF if server fetches the URL, or XSS if rendered without sanitization. +- **Fix**: Added `format: uri` and `maxLength: 2000` constraints, documented HTTPS requirement. + +## Recommendations for Server Implementation + +1. **CORS**: Restrict `Access-Control-Allow-Origin` to specific trusted domains only. Never use `*` on authenticated endpoints. +2. **SQL Injection**: Use parameterized queries/ORM for all database access, especially for search endpoints (`/api/PortalSearch/Index`, `/api/Search/FindUser`). +3. **Password Hashing**: Store passwords using bcrypt/argon2id with appropriate work factors. +4. **Token Management**: Use short-lived JWTs (15 min) with refresh tokens. Implement token revocation on logout. +5. **Audit Logging**: Log all admin operations, failed login attempts, and sensitive data access. +6. **HTTPS Only**: Enforce TLS in production; set `Strict-Transport-Security` header. +7. **Security Headers**: Add `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Content-Security-Policy`. diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..fd57243 --- /dev/null +++ b/openapi.json @@ -0,0 +1,1376 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "BankWeb API", + "version": "v1" + }, + "security": [ + { + "BearerAuth": [] + } + ], + "paths": { + "/api/Admin/Transactions": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Get all transactions (admin only)", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionRespDataTableResp" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - Admin role required" + } + } + } + }, + "/api/Admin/GetAllUsers": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Get all users (admin only)", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminUserInfoRespDataTableResp" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - Admin role required" + } + } + } + }, + "/api/AdminStore/GetAllPurchases": { + "get": { + "tags": [ + "AdminStore" + ], + "summary": "Get all purchases (admin only)", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PurchaseHistoryItemResp" + } + } + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - Admin role required" + } + } + } + }, + "/api/AdminStore/GetStoreItems": { + "get": { + "tags": [ + "AdminStore" + ], + "summary": "Get store items (admin only)", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StoreItem" + } + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - Admin role required" + } + } + } + }, + "/api/AdminStore/Create": { + "post": { + "tags": [ + "AdminStore" + ], + "summary": "Create a store item (admin only)", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StoreItemCreateReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResult" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - Admin role required" + } + } + } + }, + "/api/AdminStore/Get/{id}": { + "get": { + "tags": [ + "AdminStore" + ], + "summary": "Get a store item by ID (admin only)", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32", + "minimum": 1 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StoreItem" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - Admin role required" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/AdminStore/Edit/{id}": { + "post": { + "tags": [ + "AdminStore" + ], + "summary": "Edit a store item (admin only)", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32", + "minimum": 1 + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StoreItemCreateReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StoreItem" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - Admin role required" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/AdminStore/DeleteStoreItem": { + "post": { + "tags": [ + "AdminStore" + ], + "summary": "Delete a store item (admin only)", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteStoreItemReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResult" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - Admin role required" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/Auth/Login": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Authenticate user and return a token", + "security": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginResponse" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Invalid credentials" + }, + "429": { + "description": "Too Many Requests - Rate limited" + } + } + } + }, + "/api/Auth/Logout": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Invalidate the current session token", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResult" + } + } + } + }, + "401": { + "description": "Unauthorized" + } + } + } + }, + "/api/Auth/Register": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Register a new user account", + "security": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResult" + } + } + } + }, + "400": { + "description": "Bad Request - Validation error" + }, + "409": { + "description": "Conflict - Username already exists" + }, + "429": { + "description": "Too Many Requests - Rate limited" + } + } + } + }, + "/api/Auth/PasswordRecovery": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Request a password recovery email", + "security": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PasswordRecoveryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Success - Recovery email sent (always returns 200 to prevent user enumeration)" + }, + "429": { + "description": "Too Many Requests - Rate limited" + } + } + } + }, + "/api/Auth/Recover": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Reset password using a recovery token", + "security": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PasswordResetRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResult" + } + } + } + }, + "400": { + "description": "Bad Request - Invalid or expired token" + }, + "429": { + "description": "Too Many Requests - Rate limited" + } + } + } + }, + "/api/ContactUs/ContactUs": { + "post": { + "tags": [ + "ContactUs" + ], + "summary": "Submit a contact form message", + "security": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContactUsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResult" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too Many Requests - Rate limited" + } + } + } + }, + "/api/PortalSearch/Index": { + "get": { + "tags": [ + "PortalSearch" + ], + "summary": "Search the portal (requires authentication)", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "searchString", + "in": "query", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 200 + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "401": { + "description": "Unauthorized" + } + } + } + }, + "/api/Search/FindUser": { + "get": { + "tags": [ + "Search" + ], + "summary": "Search for a user by term (requires authentication)", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "term", + "in": "query", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "401": { + "description": "Unauthorized" + } + } + } + }, + "/api/Store/GetStoreItems": { + "get": { + "tags": [ + "Store" + ], + "summary": "Get available store items", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StoreItem" + } + } + } + } + }, + "401": { + "description": "Unauthorized" + } + } + } + }, + "/api/Store/GetHistory": { + "get": { + "tags": [ + "Store" + ], + "summary": "Get purchase history for the authenticated user", + "description": "Returns purchase history for the currently authenticated user only. The server must verify the authenticated user matches the requested username to prevent IDOR.", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PurchaseHistoryItemResp" + } + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - Cannot access other users' history" + } + } + } + }, + "/api/Store/BuyProduct": { + "post": { + "tags": [ + "Store" + ], + "summary": "Purchase a product from the store", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BuyProductReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResult" + } + } + } + }, + "400": { + "description": "Bad Request - Insufficient funds or invalid product" + }, + "401": { + "description": "Unauthorized" + } + } + } + }, + "/api/Transaction/Get/{id}": { + "get": { + "tags": [ + "Transaction" + ], + "summary": "Get a transaction by ID (only if user is sender or receiver)", + "description": "Returns transaction details only if the authenticated user is the sender or receiver. This prevents IDOR attacks.", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32", + "minimum": 1 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionResp" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - User is not a party to this transaction" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/Transaction/GetTransactions": { + "get": { + "tags": [ + "Transaction" + ], + "summary": "Get paginated transactions for the authenticated user", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "start", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "minimum": 0, + "default": 0 + } + }, + { + "name": "length", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "minimum": 1, + "maximum": 100, + "default": 10 + } + }, + { + "name": "search[value]", + "in": "query", + "schema": { + "type": "string", + "maxLength": 200 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionRespDataTableResp" + } + } + } + }, + "401": { + "description": "Unauthorized" + } + } + } + }, + "/api/Transaction/GetTransactionHistory": { + "get": { + "tags": [ + "Transaction" + ], + "summary": "Get transaction history for the authenticated user", + "description": "Returns transaction history for the currently authenticated user only. The server must derive the user from the token, not from a query parameter.", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionResp" + } + } + } + } + }, + "401": { + "description": "Unauthorized" + } + } + } + }, + "/api/Transaction/Create": { + "post": { + "tags": [ + "Transaction" + ], + "summary": "Create a new transaction (transfer funds)", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTransactionReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResult" + } + } + } + }, + "400": { + "description": "Bad Request - Insufficient funds or invalid recipient" + }, + "401": { + "description": "Unauthorized" + } + } + } + }, + "/api/User/ProfileImage": { + "get": { + "tags": [ + "User" + ], + "summary": "Get profile image for the authenticated user", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Not Found - No profile image set" + } + } + }, + "post": { + "tags": [ + "User" + ], + "summary": "Update profile image for the authenticated user", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewImageModel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request - Invalid image URL" + }, + "401": { + "description": "Unauthorized" + } + } + } + }, + "/api/User/GetAvailableFunds": { + "get": { + "tags": [ + "User" + ], + "summary": "Get available funds for the authenticated user", + "description": "Returns the balance of the currently authenticated user only. The server must derive the user from the token to prevent IDOR.", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountBalanceResp" + } + } + } + }, + "401": { + "description": "Unauthorized" + } + } + } + } + }, + "components": { + "securitySchemes": { + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "JWT token obtained from /api/Auth/Login" + } + }, + "schemas": { + "LoginRequest": { + "type": "object", + "required": ["userName", "password"], + "properties": { + "userName": { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^[a-zA-Z0-9_.-]+$" + }, + "password": { + "type": "string", + "minLength": 8, + "maxLength": 128, + "format": "password" + } + } + }, + "LoginResponse": { + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "JWT bearer token" + }, + "userName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "surname": { + "type": "string" + } + } + }, + "RegisterRequest": { + "type": "object", + "required": ["userName", "password", "name", "surname"], + "properties": { + "userName": { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^[a-zA-Z0-9_.-]+$" + }, + "password": { + "type": "string", + "minLength": 8, + "maxLength": 128, + "format": "password", + "description": "Must contain at least one uppercase, one lowercase, one digit, and one special character" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "surname": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + } + }, + "PasswordRecoveryRequest": { + "type": "object", + "required": ["userName"], + "properties": { + "userName": { + "type": "string", + "minLength": 3, + "maxLength": 50 + } + } + }, + "PasswordResetRequest": { + "type": "object", + "required": ["token", "password"], + "properties": { + "token": { + "type": "string", + "description": "One-time recovery token from the email" + }, + "password": { + "type": "string", + "minLength": 8, + "maxLength": 128, + "format": "password" + } + } + }, + "ContactUsRequest": { + "type": "object", + "required": ["userName", "message"], + "properties": { + "userName": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "message": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + } + } + }, + "TransactionResp": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "senderId": { + "type": "string" + }, + "receiverId": { + "type": "string" + }, + "dateTime": { + "type": "string", + "format": "date-time" + }, + "reason": { + "type": "string" + }, + "amount": { + "type": "number", + "format": "double" + }, + "reference": { + "type": "string" + }, + "senderName": { + "type": "string" + }, + "senderSurname": { + "type": "string" + }, + "receiverName": { + "type": "string" + }, + "receiverSurname": { + "type": "string" + } + } + }, + "TransactionRespDataTableResp": { + "type": "object", + "properties": { + "recordsTotal": { + "type": "integer", + "format": "int32" + }, + "recordsFiltered": { + "type": "integer", + "format": "int32" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionResp" + } + } + } + }, + "AdminUserInfoResp": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "surname": { + "type": "string" + }, + "username": { + "type": "string" + }, + "role": { + "type": "integer", + "format": "int32" + } + } + }, + "AdminUserInfoRespDataTableResp": { + "type": "object", + "properties": { + "recordsTotal": { + "type": "integer", + "format": "int32" + }, + "recordsFiltered": { + "type": "integer", + "format": "int32" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminUserInfoResp" + } + } + } + }, + "PurchaseHistoryItemResp": { + "type": "object", + "properties": { + "purchaseTime": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "price": { + "type": "number", + "format": "double" + } + } + }, + "StoreItem": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "price": { + "type": "number", + "format": "double" + }, + "installments": { + "type": "integer", + "format": "int32" + } + } + }, + "StoreItemCreateReq": { + "type": "object", + "required": ["name", "price"], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "type": "string", + "maxLength": 2000 + }, + "price": { + "type": "number", + "format": "double", + "minimum": 0.01 + }, + "installments": { + "type": "integer", + "format": "int32", + "minimum": 1, + "maximum": 60 + } + } + }, + "DeleteStoreItemReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "format": "int32", + "minimum": 1 + } + } + }, + "EmptyResult": { + "type": "object" + }, + "BuyProductReq": { + "type": "object", + "required": ["id", "quantity"], + "properties": { + "id": { + "type": "integer", + "format": "int32", + "minimum": 1 + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 1, + "maximum": 100 + } + } + }, + "CreateTransactionReq": { + "type": "object", + "required": ["receiverId", "amount"], + "properties": { + "receiverId": { + "type": "string", + "minLength": 1, + "maxLength": 50, + "description": "Username of the recipient" + }, + "reason": { + "type": "string", + "maxLength": 500 + }, + "amount": { + "type": "number", + "format": "double", + "minimum": 0.01, + "maximum": 1000000, + "description": "Transfer amount (must be positive and within user's available balance)" + }, + "reference": { + "type": "string", + "maxLength": 100 + } + } + }, + "NewImageModel": { + "type": "object", + "required": ["imageUrl"], + "properties": { + "imageUrl": { + "type": "string", + "format": "uri", + "maxLength": 2000, + "description": "Must be a valid HTTPS URL pointing to an image resource" + } + } + }, + "AccountBalanceResp": { + "type": "object", + "properties": { + "balance": { + "type": "number", + "format": "double" + } + } + } + } + } +}