vuln-bank: a banking app with the flaws left in on purpose
A deliberately vulnerable banking application designed for practicing Security Testing of Web App, APIs, AI integrated App and secure code reviews. Features common vulnerabilities found in real-world applications, making it an ideal platform for security professionals, developers, and enthusiasts to learn pentesting and secure coding practices.
At a glance
- What is it?
- vuln-bank is a Flask banking application with a long list of deliberate vulnerabilities, including SQL injection, weak JWT handling, BOLA, prompt injection in an AI support agent and GraphQL resolver injection. It suits a security team practising detection and secure review, and the README says it should only run in isolated environments.
- Who is it for?
- Adopt vuln-bank as a training target when you want a single application that exercises web, API, GraphQL and LLM security findings rather than a set of isolated challenges, because the banking domain makes the flaws feel like real workflows instead of lab exercises. Do not deploy it anywhere reachable, because the warning is explicit and the vulnerabilities are real.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 31 days ago.
- What is it written in?
- Mainly HTML, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One application, eleven vulnerability families
vuln-bank is a deliberately vulnerable web application for practising application security testing of the web, APIs and LLMs, secure code review, and implementing security in CI/CD pipelines. The README warns that it is intentionally vulnerable and should only be used for educational purposes in isolated environments.
The domain is a small bank. There is user authentication and authorisation, account balance management, money transfers, loan requests, profile picture upload, transaction history, a GraphQL backed transaction analytics dashboard, a password reset system using a 3-digit PIN, multi-currency virtual card management, a public merchant payment API for intentionally vulnerable ecommerce and demo integrations, a bill payments system, and an AI customer support agent backed by a real LLM through the DeepSeek API with a mock mode.
What makes the target useful is the spread of the flaws rather than their count. The authentication and authorisation group alone covers SQL injection in login, a weak JWT implementation, broken object level authorisation, broken object property level authorisation, mass assignment with excessive data exposure, a 3-digit password reset PIN, tokens in localStorage, and no server-side token invalidation or session expiration.
Then there is the data security group with information disclosure, sensitive data exposure, plaintext password storage and detailed error messages. The transaction group adds no amount validation, negative transfers, no limits, race conditions in transfers and balance updates, and no validation on recipient accounts.
The file operations group is the classic set, unrestricted upload, path traversal, no type validation, no size limits, unsafe naming, and server-side request forgery through a URL-based profile image import. The session group adds weak secret keys and token exposure in URLs. And the client and server side group carries XSS, CSRF, insecure direct object references and no rate limiting.
Three groups are newer additions and give the project its current shape. The virtual card group includes mass assignment in card limit updates and in exchange rate handling, predictable card number generation, plaintext card detail storage, a client-controlled currency conversion during funding, and no transaction verification. The merchant payment API group covers plaintext merchant passwords, API keys returned in registration and login responses, raw card number and CVV accepted by the API, missing idempotency and replay protection, and predictable authorisation code generation. And the AI customer support group lists prompt injection, AI-based information disclosure, broken authorisation in an AI context, insufficient input validation for prompts, direct database access through AI manipulation, role override attacks, context injection and exposed system prompts, the first five with CWE numbers.
GraphQL, the AI agent and the queries behind them
Two surfaces in the application are worth pulling out, because they are the parts a general web testing course does not cover.
The GraphQL group has six entries. Schema introspection is enabled on the transaction analytics endpoint, which means the whole query surface can be enumerated in one request. The weak JWT-based authentication is inherited by the /graphql route, so the same token flaws reach the API. There is SQL injection in the GraphQL resolver query construction, which is a different injection surface from a REST parameter because the query is assembled in the resolver. There are no depth or complexity controls, so a nested query can be used to amplify work. Raw GraphQL errors are disclosed. And transaction analytics are exposed through admin-scoped queries, meaning the field-level authorisation is what stands between one user and another's spending.
The AI group is the other one. The customer support agent is a real LLM through the DeepSeek API, with a mock mode for when you do not have a key, and the vulnerabilities listed are the ones that appear when a language model is given access to a database and a set of tools. Prompt injection is the headline one, and the list goes further than injection: AI-based information disclosure, broken authorisation in an AI context where the model enforces permissions on the user's behalf, insufficient input validation for prompts, direct database access through AI manipulation, role override attacks that talk the model out of its instructions, context injection, AI-assisted unauthorised data access, and exposed system prompts and configurations.
That last group is the reason the project exists in its current form. A scanner finds the SQL injection and the weak secret. It does not find out that the support bot will run a query for whoever types the right thing into the chat box, because the injection is into a prompt and the payoff is a database access, not a syntax error in a request.
Running it, and the container behaviour that is deliberate
The prerequisites are Docker and Docker Compose for the containerised setup, PostgreSQL and Python 3.9 or higher for a local run, and Git. The recommended route is Docker Compose:
git clone https://github.com/Commando-X/vuln-bank.git
cd vuln-bankdocker-compose up -d --buildand the application is then available at http://localhost:5000.
The container recovery behaviour section is the part to read carefully, because several settings look like mistakes and are not. The web and db containers use restart: unless-stopped, so Docker restarts them if the process exits. The db service exposes a health check and web waits for Postgres readiness before starting. The web container runs the Flask development server with debug=True, and the README marks that as intentional, because it preserves the training scenarios that target the Werkzeug debugger. And web exposes GET /healthz so the container can report whether the app and the database are actually usable.
The one behaviour that will surprise you mid-session is the xss-cleaner. It preserves the intentional stored XSS behaviour, but neutralises stored username, bio and transaction-description payloads after fifteen minutes, and you adjust the window and the sweep frequency with XSS_PAYLOAD_TTL_MINUTES and XSS_CLEANUP_INTERVAL_SECONDS. The reasoning is stated: it keeps the intentionally vulnerable application behaviour intact while making the container lifecycle less fragile.
So if you start a session, explore for a while, and come back to a payload that no longer fires, that is the sweeper, not a fix to the app.
There is also a local smoke test that validates the runtime wiring without starting real containers:
python3 -m unittest discover -s tests -vwhich checks the /healthz endpoint behaviour and verifies the rest of the local wiring.
The stack, and how a small Flask app got this much surface
The application is a single Flask service with a PostgreSQL database, and the requirement pins are exact rather than loose.
flask==2.0.1, werkzeug==2.0.1 and pyjwt==2.4.0 are the three that matter most for the flaws on display. The Werkzeug pin is what makes the debugger scenario reproducible, and the PyJWT pin is what the weak JWT implementation is written against. Then flask-swagger-ui==4.11.1 for the API documentation, flask-cors==4.0.0, psycopg2-binary==2.9.9 for the database, python-dotenv==1.0.0, requests==2.31.0 and graphene==3.3 for the GraphQL layer.
The source layout is a set of flat Python modules at the repository root, which is what a small Flask application looks like before anyone organises it: app.py, auth.py, database.py, merchant_payments.py, transaction_graphql.py, username_validation.py, exposed_env_files.py, xss_cleanup.py and ai_agent_deepseek.py, alongside graphql/ for the schema, static/ and templates/ for the front end, tests/ for the suite, and start.sh as the entry point. Dockerfile and docker-compose.yml are the deployment.
Two of those root modules are worth naming. exposed_env_files.py is a deliberate data exposure vulnerability in its own file, which is a design choice rather than an accident. And ai_agent_deepseek.py is the AI support agent, kept separate so the model integration and its prompt handling can be read in isolation.
The container image is python:3.9-slim with the postgresql-client installed, the requirements installed at build time, the uploads directory created and given wide permissions, and a healthcheck that hits the /healthz endpoint. The Compose file adds an autoheal service that restarts containers Docker marks unhealthy, which together with the xss-cleaner and the health checks makes this a training environment that recovers on its own, unlike the usual throwaway vulnerable app.
Editorial conclusion
Adopt vuln-bank as a training target when you want a single application that exercises web, API, GraphQL and LLM security findings rather than a set of isolated challenges, because the banking domain makes the flaws feel like real workflows instead of lab exercises. Do not deploy it anywhere reachable, because the warning is explicit and the vulnerabilities are real. Before you start, read the container behaviour notes, since the web container runs the Flask development server with debug enabled on purpose to keep the Werkzeug debugger scenarios working, and remember that the stored XSS payloads are swept after fifteen minutes so a long session will find some evidence already neutralised.
Frequently asked questions
What kinds of vulnerabilities does vuln-bank contain?
The README lists eleven groups: authentication flaws including SQL injection in login, weak JWT, BOLA, BOPLA and mass assignment; data security issues such as plaintext password storage; transaction flaws including negative transfers and race conditions; file operation flaws including unrestricted upload, path traversal and SSRF; session management flaws; XSS and CSRF; virtual card flaws; merchant payment API flaws; AI customer support flaws including prompt injection and direct database access through AI manipulation; and GraphQL flaws including resolver query injection and missing depth controls.
How do I run vuln-bank locally?
Clone the repository, then run `docker-compose up -d --build`, and the application is available at http://localhost:5000. You can also validate the local wiring without containers with `python3 -m unittest discover -s tests -v`. Docker and Docker Compose are the recommended prerequisites, and the README warns the app should only be used in isolated environments.
Why does vuln-bank run Flask with debug enabled?
The README states the web container runs the Flask development server with debug=True intentionally, because it preserves the training scenarios that target the Werkzeug debugger. The web and db containers also use restart: unless-stopped, and web exposes a GET /healthz endpoint so the container can report whether the app and database are usable.
What is the xss-cleaner container in vuln-bank for?
It preserves the intentional stored XSS behaviour but neutralises stored username, bio and transaction-description payloads after fifteen minutes, with the window and sweep frequency set by XSS_PAYLOAD_TTL_MINUTES and XSS_CLEANUP_INTERVAL_SECONDS. The stated aim is to keep the vulnerable application behaviour intact while making the container lifecycle less fragile.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/commando-x-vuln-bank)Was this useful?