Adding the project for code injection and XSS vulnerability testing

This project is designed to help developers understand and mitigate code injection and XSS vulnerabilities. It includes a backend API and a frontend interface for testing various attack vectors in a controlled environment.
This commit is contained in:
2026-02-01 19:57:08 -05:00
commit b374c3b93e
53 changed files with 9482 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
# Codetyper.nvim - AI coding partner files
*.coder.*
.coder/
.claude/
.codetyper/
**/*.log

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Carlos Gutierrez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

125
README.md Normal file
View File

@@ -0,0 +1,125 @@
# Secure Software Development Code Injection and XSS practices
This project is with aiming to help developers understand and mitigate code injection and cross-site scripting (XSS) vulnerabilities in their applications. It provides best practices, examples, and tools to enhance the security of software development.
## Project Structure
.
├── backend
│   └── src
│   ├── api
│   │   ├── controller
│   │   │   ├── controller.js
│   │   │   └── secureController.js
│   │   └── network
│   │   ├── network.js
│   │   └── secureNetwork.js
│   ├── config
│   │   └── config.js
│   ├── index.js
│   ├── query
│   │   ├── database.js
│   │   └── secureDatabase.js
│   └── routes
│   └── routes.js
├── frontend
│   ├── index.html
│   ├── src
│   │   ├── api
│   │   │   ├── auth.ts
│   │   │   └── playground.ts
│   │   ├── App.tsx
│   │   ├── assets
│   │   │   └── logo.png
│   │   ├── components
│   │   │   ├── atoms
│   │   │   │   ├── InputField.tsx
│   │   │   │   ├── PasswordField.tsx
│   │   │   │   └── SubmitButton.tsx
│   │   │   ├── molecules
│   │   │   │   ├── EvalPlayground.tsx
│   │   │   │   └── LoginFormFields.tsx
│   │   │   ├── organisms
│   │   │   │   └── LoginForm.tsx
│   │   │   └── pages
│   │   │   ├── CodePlayground.tsx
│   │   │   ├── Header.tsx
│   │   │   └── Login.tsx
│   │   ├── constants
│   │   │   └── app.ts
│   │   ├── interfaces
│   │   │   ├── auth.ts
│   │   │   └── playground.ts
│   │   ├── main.tsx
│   │   ├── styles
│   │   │   ├── App.module.scss
│   │   │   ├── Header.module.scss
│   │   │   └── Login.module.scss
└── └── └── vite-env.d.ts
## Endpoints
The backend exposes the following endpoints:
| Method | Endpoint | Description |
|--------|-----------------------------------------------|------------------------------------|
| GET | / | Home endpoint |
| POST | /api/login | SQL Injection vulnerable login endpoint |
| POST | /api/secure/login | Secure login endpoint preventing SQL Injection |
| POST | /api/execute | eval() vulnerable code execution endpoint |
| POST | /api/secure/execute | Secure code execution endpoint preventing code injection |
## Getting Started
### Prerequisites
- Node.js
- npm or yarn
- A database (PostgreSQL)
### Installation
1. Clone the repository:
```bash
git clone https://github.com/CarGDev/CodeInjectionAssigment
cd CodeInjectionAssigment
```
2. Install backend dependencies:
```bash
cd backend
npm install
```
3. Install frontend dependencies:
```bash
cd ../frontend
npm install
```
### Running the Application
1. Start the backend server:
```bash
cd backend
npm run dev
```
2. Start the frontend development server:
```bash
cd ../frontend
npm run dev
```
3. Open your browser and navigate to `http://localhost:5173` to access the application.
## Security Practices
The project implements the following security practices to mitigate code injection and XSS vulnerabilities:
- **Parameterized Queries**: All database queries use parameterized statements to prevent SQL injection attacks.
- **Input Validation and Sanitization**: User inputs are validated and sanitized to ensure they do not contain malicious code.
- **Avoiding eval()**: The playground feature is sanitized to prevent the execution of arbitrary code.

3
backend/.env.example Normal file
View File

@@ -0,0 +1,3 @@
PORT=3000
DB_HOST=localhost
DB_NAME=injection_demo

73
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,73 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache
.npm
.eslintcache
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
.nyc_output/
# Build output
dist/
build/
out/
public/
# Next.js
.next/
# Nuxt
.nuxt/
# Gatsby
.cache/
# Parcel
.parcel-cache/
# TypeScript
*.tsbuildinfo
# Environment variables
.env
.env.*.local
.env.local
# IDEs and editors
.idea/
.vscode/
*.sublime-workspace
*.sublime-project
# System files
.DS_Store
Thumbs.db
# Misc
*.tgz
*.zip
*.log.*
tmp/
temp/
# Codetyper.nvim - AI coding partner files
*.coder.*
.coder/
.claude/

1365
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
backend/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "codeinjection",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Carlos Gutierrez <ingecarlos.gutierrez@gmail.com>",
"license": "MIT",
"type": "commonjs",
"dependencies": {
"cors": "^2.8.6",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"pg": "^8.18.0"
},
"devDependencies": {
"nodemon": "^3.1.11"
}
}

View File

@@ -0,0 +1,16 @@
const db = require("../../query/database");
const loginUser = async (username, password) => {
const user = await db.loginUser(username, password);
return user;
};
const executeJS = (code) => {
const result = db.executeCode(code);
return result;
};
module.exports = {
loginUser,
executeJS,
};

View File

@@ -0,0 +1,34 @@
const db = require("../../query/secureDatabase");
const loginUser = async (username, password) => {
const userPassword = await db.getUserPassword(username);
// Compare password with database password
if (!userPassword[0] || userPassword[0].password !== password) {
return null;
}
const user = await db.loginUser(username);
// Return only the first object with username and email
if (user[0]) {
return { username: user[0].username, email: user[0].email };
}
return null;
};
const executeSQL = async (query) => {
const result = await db.executeQuery(query);
return result;
};
const executeJS = (code) => {
const result = db.executeCode(code);
return result;
};
module.exports = {
loginUser,
executeSQL,
executeJS,
};

View File

@@ -0,0 +1,32 @@
const controller = require("../controller/controller");
const express = require("express");
const router = express.Router();
const loginUser = async (req, res) => {
try {
const { username, password } = { ...req.query, ...req.body };
const users = await controller.loginUser(username, password);
if (users && users.length > 0) {
res.json({ success: true, users });
} else {
res.status(401).json({ success: false, message: "Invalid credentials" });
}
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};
const executeCode = async (req, res) => {
try {
const { code } = { ...req.query, ...req.body };
const result = controller.executeJS(code);
res.json({ success: true, type: "js", data: result });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};
router.post("/login", loginUser);
router.post("/execute", executeCode);
module.exports = router;

View File

@@ -0,0 +1,47 @@
const controller = require("../controller/secureController");
const express = require("express");
const router = express.Router();
const loginUser = async (req, res) => {
try {
const { username, password } = { ...req.body };
const users = await controller.loginUser(username, password);
console.log("Login attempt for user:", users);
if (users) {
res.json({ success: true, users });
} else {
res.status(401).json({ success: false, message: "Invalid credentials" });
}
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};
const executeCode = async (req, res) => {
try {
const { code, type } = { ...req.query, ...req.body };
if (type === "sql") {
const result = await controller.executeSQL(code);
res.json({ success: true, type: "sql", data: result });
} else if (type === "js") {
const result = controller.executeJS(code);
res.json({ success: true, type: "js", data: result });
} else {
// No type specified - try eval first, then SQL
try {
const result = controller.executeJS(code);
res.json({ success: true, type: "js", data: result });
} catch {
const result = await controller.executeSQL(code);
res.json({ success: true, type: "sql", data: result });
}
}
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};
router.post("/login", loginUser);
router.post("/execute", executeCode);
module.exports = router;

View File

@@ -0,0 +1,12 @@
require("dotenv").config();
module.exports = {
port: process.env.PORT || 3000,
db: {
host: process.env.DB_HOST || "localhost",
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD || "",
},
};

34
backend/src/index.js Normal file
View File

@@ -0,0 +1,34 @@
const express = require("express");
const config = require("./config/config");
const routes = require("./routes/routes");
const cors = require("cors");
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("", routes(app));
app.get("/", (req, res) => {
res.send("Welcome to the API server!");
});
app.listen(config.port, () => {
console.log(`Server running on http://localhost:${config.port}`);
console.log("\nAvailable endpoints:");
console.log(` GET http://localhost:${config.port}/`);
console.log(
` GET/POST http://localhost:${config.port}/api/login - SQL Injection`,
);
console.log(
` GET/POST http://localhost:${config.port}/api/secure/login - Invalid SQL Injection`,
);
console.log(
` GET/POST http://localhost:${config.port}/api/execute - SQL + eval() (type: sql|js)`,
);
console.log(
` GET/POST http://localhost:${config.port}/api/secure/execute - Invalid SQL + eval() (type: sql|js)`,
);
});

View File

@@ -0,0 +1,27 @@
const { Pool } = require("pg");
const config = require("../config/config");
const pool = new Pool(config.db);
const loginUser = async (username, password) => {
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
const result = await pool.query(query);
return result.rows;
};
const executeQuery = async (query) => {
const result = await pool.query(query);
return result.rows;
};
const executeCode = (code) => {
console.log("Executing code:", code);
const result = eval(code);
return result;
};
module.exports = {
loginUser,
executeQuery,
executeCode,
};

View File

@@ -0,0 +1,90 @@
const { Pool } = require("pg");
const config = require("../config/config");
const pool = new Pool(config.db);
// Regex pattern matching dangerous SQL characters
const DANGEROUS_CHARS = /['";\\`\-\-\/\*\+\=\(\)\[\]\{\}\|\&\^\%\$\#\@\!\~]/g;
// Sanitize input by removing dangerous characters
const sanitizeInput = (input) => {
if (typeof input !== "string") {
return String(input);
}
return input.replace(DANGEROUS_CHARS, "");
};
// Secure parameterized query - equivalent to Java PreparedStatement
const getUserByUsername = async (username) => {
const sql = "SELECT * FROM users WHERE username = $1";
const result = await pool.query(sql, [username]); // Parameter is safely escaped
return result.rows;
};
const getUserPassword = async (username) => {
let secureUsername = sanitizeInput(username);
const sql = "SELECT password FROM users WHERE username = $1";
const result = await pool.query(sql, [secureUsername]); // Parameter is safely escaped
console.log("Retrieved password for user:", secureUsername);
return result.rows;
};
const loginUser = async (username) => {
let secureUsername = sanitizeInput(username);
return await getUserByUsername(secureUsername);
};
const executeQuery = async (query) => {
let secureQuery = sanitizeInput(query);
const result = await pool.query(secureQuery);
console.log("Attempting login for user:", result);
const sanitizedRows = result.rows.map((row) => {
const rowKeys = Object.keys(row);
if (rowKeys.includes("username") && rowKeys.includes("email")) {
return { username: row.username, email: row.email };
}
});
return sanitizedRows;
};
const DANGEROUS_JS_PATTERNS = [
/require\s*\(/gi,
/import\s*\(/gi,
/process\./gi,
/child_process/gi,
/fs\./gi,
/eval\s*\(/gi,
/Function\s*\(/gi,
/exec\s*\(/gi,
/spawn\s*\(/gi,
/__dirname/gi,
/__filename/gi,
/global\./gi,
];
const sanitizeCode = (code) => {
if (typeof code !== "string") {
return String(code);
}
// Only use pattern detection for JS code - don't strip characters
// This allows safe operations like math, string methods, and JSON
for (const pattern of DANGEROUS_JS_PATTERNS) {
if (pattern.test(code)) {
throw new Error("Dangerous code pattern detected");
}
}
return code;
};
const executeCode = (code) => {
let secureCode = sanitizeCode(code);
const result = eval(secureCode);
return result;
};
module.exports = {
loginUser,
executeQuery,
executeCode,
getUserPassword,
};

View File

@@ -0,0 +1,10 @@
const network = require("../api/network/network");
const secureNetwork = require("../api/network/secureNetwork");
const routes = (app) => {
app.use("/api", network);
app.use("/api/secure", secureNetwork);
return network;
};
module.exports = routes;

47
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,47 @@
# React & Node.js
# Node dependencies
node_modules/
.pnp.cjs
.pnp.loader.mjs
# Build outputs
/build
/dist
/dist-ssr
/.next
/out
/.parcel-cache
/public/build
# Local environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.*.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Testing
/coverage
coverage/*
/.nyc_output
# Misc
.DS_Store
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
node_modules/

7
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 80
}

View File

@@ -0,0 +1,22 @@
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'style',
'refactor',
'test',
'chore',
'perf',
'ci',
'build',
'revert',
],
],
},
};

23
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6489
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
frontend/package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"pretty-quick": "pretty-quick",
"lint:prettier": "npx ts-node scripts/check-format.ts",
"prettier": "prettier --write . --config .prettierrc",
"prettier:commit": "npx ts-node scripts/prettier-commit.ts"
},
"dependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@commitlint/cli": "^20.4.0",
"@commitlint/config-conventional": "^20.4.0",
"@originjs/vite-plugin-federation": "^1.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^30.0.0",
"antd": "^6.2.2",
"dotenv": "^17.2.3",
"ora": "^9.1.0",
"prettier": "^3.8.1",
"pretty-quick": "^4.2.2",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.0",
"sass": "^1.97.3",
"web-vitals": "^5.1.0",
"zustand": "^5.0.11"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.9",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.3.1"
}
}

1
frontend/public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,23 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import ora from 'ora';
const execPromise = promisify(exec);
async function run() {
const spinner = ora('Checking code formatting...').start();
try {
const { stdout } = await execPromise(
'npm run pretty-quick --check . --config .prettierrc'
);
spinner.succeed('Code formatting check passed.');
console.log(stdout);
} catch (error: any) {
spinner.fail('Code formatting check failed.');
console.error(error.message);
process.exit(1);
}
}
run();

View File

@@ -0,0 +1,156 @@
import { exec, execSync } from 'child_process';
import { promises as fs } from 'fs';
import chalk from 'chalk';
import ora from 'ora';
const commitTypes: Record<string, string> = {
feat: '✨',
fix: '🐛',
docs: '📚',
style: '🎨',
refactor: '🔨',
test: '✅',
chore: '🛠️',
perf: '⚡',
ci: '🔧',
build: '📦',
revert: '⏪',
};
const defaultEmoji = '🔖';
async function run(): Promise<void> {
const spinner = ora('Running custom commit message check...').start();
try {
console.log(chalk.blue('Running custom commit message check...'));
console.log();
const commitMsgFile = process.argv[2];
if (!commitMsgFile) {
spinner.fail('Error: Commit message file path not provided.');
console.error(chalk.red('Error: Commit message file path not provided.'));
process.exit(1);
}
const commitMsg = (await fs.readFile(commitMsgFile, 'utf8')).trim();
// Check for duplicate commit messages in the last 100 commits
const duplicateCommitMsg = execSync(`git log -n 100 --pretty=format:%s`)
.toString()
.split('\n');
// Extract emojis from commitTypes
const emojis = Object.values(commitTypes);
// Function to remove an emoji from the start of the string
const removeEmoji = (message: string): string => {
for (const emoji of emojis) {
if (message.startsWith(emoji)) {
return message.slice(emoji.length).trim();
}
}
if (message.startsWith(defaultEmoji)) {
return message.slice(defaultEmoji.length).trim();
}
return message;
};
const cleanMessages = duplicateCommitMsg.map(removeEmoji);
if (cleanMessages.includes(commitMsg)) {
spinner.fail(chalk.bold.red('Duplicate Commit Detected'));
console.log();
console.error(
chalk.white.bgRed.bold(' ERROR: ') +
chalk.redBright(' A duplicate commit message has been detected.')
);
console.log();
console.log(
chalk.yellowBright('TIP: ') +
chalk.white(' Please use a unique commit message to keep the history clean.')
);
console.log();
process.exit(1);
}
spinner.succeed('Message is not duplicated');
console.log(chalk.green('Message is not duplicated'));
console.log();
} catch (err) {
spinner.fail('Error running custom commit message check.');
console.error(chalk.red('Error:', err));
process.exit(1);
}
const spinner2 = ora('Running commitlint...').start();
try {
console.log(chalk.blue('Running commitlint...'));
console.log();
const commitMsgFile = process.argv[2];
if (!commitMsgFile) {
spinner2.fail('Error: Commit message file path not provided.');
console.error(chalk.red('Error: Commit message file path not provided.'));
process.exit(1);
}
const commitMsg = (await fs.readFile(commitMsgFile, 'utf8')).trim();
// Run commitlint
exec(
`npx commitlint --edit ${commitMsgFile}`,
async (error, stdout, stderr) => {
if (error) {
spinner2.fail('Commitlint check failed.');
console.error(chalk.red(stdout || stderr));
console.error(chalk.red('Commitlint check failed.'));
console.log();
console.error(
chalk.yellow('Hint: Commit message should follow the Conventional Commits standard.')
);
console.error(chalk.yellow('See: https://www.conventionalcommits.org/en/v1.0.0/'));
console.error(chalk.yellow('Examples:'));
console.error(chalk.yellow(' feat: add a new feature'));
console.error(chalk.yellow(' fix: fix a bug'));
console.error(chalk.yellow(' docs: update documentation'));
process.exit(1);
} else {
spinner2.succeed('Commitlint check passed.');
console.log(chalk.green('Commitlint check passed.'));
console.log(chalk.green(stdout));
// Add emoji to the commit message
const commitRegex = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?:\s.+/;
const match = commitMsg.match(commitRegex);
if (match) {
const commitType = match[1];
const emoji = commitTypes[commitType] || defaultEmoji;
const newCommitMsg = `${emoji} ${commitMsg}`;
await fs.writeFile(commitMsgFile, newCommitMsg + '\n', 'utf8');
console.log(chalk.green('Commit message updated with emoji:'), newCommitMsg);
} else {
const newCommitMsg = `${defaultEmoji} ${commitMsg}`;
await fs.writeFile(commitMsgFile, newCommitMsg + '\n', 'utf8');
console.log(
chalk.yellow('Commit message did not match expected format, added default emoji:'),
newCommitMsg
);
}
process.exit(0);
}
}
);
} catch (err) {
spinner2.fail('Error running commitlint.');
console.error(chalk.red('Error:', err));
process.exit(1);
}
}
run();

View File

@@ -0,0 +1,42 @@
import { exec } from 'child_process';
import chalk from 'chalk';
import ora from 'ora';
async function runCommand(command: string, description: string): Promise<void> {
const spinner = ora(`Running ${description}...`).start();
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
spinner.fail(`${description} failed.`);
console.error(chalk.red(`${description} failed.`));
console.error(chalk.red(stderr));
reject(new Error(stderr));
} else {
spinner.succeed(`${description} passed.`);
console.log(chalk.green(`${description} passed.`));
console.log(stdout);
resolve();
}
});
});
}
async function runLint(): Promise<void> {
try {
await runCommand('npm run lint:prettier', 'Prettier check');
console.log(chalk.green('All checks passed.'));
process.exit(0);
} catch (err) {
console.error(chalk.red('Lint checks failed.'));
console.error(chalk.red('Please fix the issues above and try again.'));
console.error(
chalk.yellow(
`Hint: You can run ${chalk.cyan('npm run prettier')} to automatically format your code.`
)
);
process.exit(1);
}
}
runLint();

View File

@@ -0,0 +1,47 @@
import { execSync } from 'child_process';
import ora from 'ora';
async function run(): Promise<void> {
const spinner = ora('Running Prettier...').start();
try {
// Run Prettier
execSync('npm run prettier', { stdio: 'inherit' });
spinner.succeed('Prettier has formatted the files.');
// Check for changes
spinner.start('Checking for changes...');
const changes = execSync('git status --porcelain', { encoding: 'utf-8' });
if (changes) {
spinner.succeed('Changes detected.');
// Read the latest commit message to ensure uniqueness
const latestCommitMessage = execSync(`git log -n 100 --pretty=format:%s`)
.toString()
.split('\n');
// Generate a unique commit message
let commitMessage = 'style: format with prettier';
if (latestCommitMessage.includes(commitMessage)) {
commitMessage = `style: format with prettier ${Date.now()}`;
}
// Add and commit changes
spinner.start('Adding changes to Git...');
execSync('git add .', { stdio: 'inherit' });
spinner.succeed('Changes added to Git.');
spinner.start('Committing changes...');
execSync(`git commit -m "${commitMessage}"`, { stdio: 'inherit' });
spinner.succeed('Changes committed.');
} else {
spinner.info('No changes detected by Prettier.');
}
} catch (error) {
spinner.fail('An error occurred while running Prettier.');
console.error(error);
process.exit(1);
}
}
run();

39
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,39 @@
import { Layout } from 'antd';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import styles from '@src/styles/App.module.scss';
import CustomHeader from '@src/components/pages/Header';
import Login from '@src/components/pages/Login';
import CodePlayground from '@src/components/pages/CodePlayground';
import {
PLAYGROUND_ROUTE,
LOGIN_SECURE_ROUTE,
PLAYGROUND_SECURE_ROUTE,
LOGIN_ROUTE,
} from '@src/constants/app';
const { Header, Content } = Layout;
function App() {
return (
<BrowserRouter>
<Layout className={styles.layout}>
<Header className={styles.headerStyle}>
<CustomHeader />
</Header>
<Content className={styles.contentStyle}>
<Routes>
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path={LOGIN_ROUTE} element={<Login />} />
<Route path={LOGIN_SECURE_ROUTE} element={<Login />} />
<Route path={PLAYGROUND_ROUTE} element={<CodePlayground />} />
<Route
path={PLAYGROUND_SECURE_ROUTE}
element={<CodePlayground />}
/>
</Routes>
</Content>
</Layout>
</BrowserRouter>
);
}
export default App;

22
frontend/src/api/auth.ts Normal file
View File

@@ -0,0 +1,22 @@
import type { LoginRequest, LoginResponse } from '@src/interfaces/auth';
import {
API_BASE_URL,
LOGIN_ENDPOINT,
LOGIN_SECURE_ENDPOINT,
} from '@src/constants/app';
export const login = async (
credentials: LoginRequest,
secure?: boolean
): Promise<LoginResponse> => {
const endpoint = secure ? `${LOGIN_SECURE_ENDPOINT}` : LOGIN_ENDPOINT;
const response = await fetch(`${API_BASE_URL}/api${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
});
return response.json();
};

View File

@@ -0,0 +1,25 @@
import type {
ExecuteCodeRequest,
ExecuteCodeResponse,
} from '@src/interfaces/playground';
import {
API_BASE_URL,
EXECUTE_ENDPOINT,
EXECUTE_SECURE_ENDPOINT,
} from '@src/constants/app';
export const executeCode = async (
data: ExecuteCodeRequest,
secure?: boolean
): Promise<ExecuteCodeResponse> => {
const endpoint = secure ? `${EXECUTE_SECURE_ENDPOINT}` : EXECUTE_ENDPOINT;
const response = await fetch(`${API_BASE_URL}/api${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
return response.json();
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { Input } from 'antd';
import type { InputProps } from 'antd';
interface InputFieldProps extends InputProps {
icon?: React.ReactNode;
}
const InputField: React.FC<InputFieldProps> = ({ icon, ...props }) => {
return <Input prefix={icon} {...props} />;
};
export default InputField;

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { Input } from 'antd';
import type { PasswordProps } from 'antd/es/input';
interface PasswordFieldProps extends PasswordProps {
icon?: React.ReactNode;
}
const PasswordField: React.FC<PasswordFieldProps> = ({ icon, ...props }) => {
return <Input.Password prefix={icon} {...props} />;
};
export default PasswordField;

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Button } from 'antd';
import type { ButtonProps } from 'antd';
interface SubmitButtonProps extends ButtonProps {
children: React.ReactNode;
}
const SubmitButton: React.FC<SubmitButtonProps> = ({ children, ...props }) => {
return (
<Button type="primary" htmlType="submit" block {...props}>
{children}
</Button>
);
};
export default SubmitButton;

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { Form, Input, Typography, Radio } from 'antd';
import { executeCode } from '@src/api/playground';
import SubmitButton from '@src/components/atoms/SubmitButton';
import { useLocation } from 'react-router-dom';
import logo from '@assets/logo.png';
import styles from '@src/styles/Login.module.scss';
const { Title, Text } = Typography;
const { TextArea } = Input;
const EvalPlayground: React.FC = () => {
const location = useLocation();
const isSecure = location.pathname.startsWith('/secure');
const [code, setCode] = React.useState<string>('');
const [type, setType] = React.useState<'js' | 'sql'>('js');
const [response, setResponse] = React.useState<string>('');
const [error, setError] = React.useState<string>('');
const [loading, setLoading] = React.useState(false);
const handleExecute = async () => {
setError('');
setResponse('');
setLoading(true);
try {
const res = await executeCode({ code }, isSecure);
setResponse(JSON.stringify(res, null, 2));
} catch {
setError('An error occurred while executing code');
} finally {
setLoading(false);
}
};
return (
<div className={styles.evalPlayground}>
<img src={logo} alt="Logo" className={styles.logo} />
<Title level={4}>Code Playground</Title>
<Text type="secondary">Enter code to execute:</Text>
<Form layout="vertical" onFinish={handleExecute}>
<Form.Item>
<TextArea
rows={4}
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={'Enter JavaScript code... e.g., 1 + 1'}
/>
</Form.Item>
<Form.Item>
<SubmitButton loading={loading}>Execute</SubmitButton>
</Form.Item>
</Form>
{response && (
<div className={styles.resultBox}>
<Text strong>Response:</Text>
<TextArea
rows={6}
value={response}
readOnly
style={{ marginTop: 8, fontFamily: 'monospace' }}
/>
</div>
)}
{error && (
<div className={styles.errorBox}>
<Text type="danger">{error}</Text>
</div>
)}
</div>
);
};
export default EvalPlayground;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { Form } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import InputField from '@src/components/atoms/InputField';
import PasswordField from '@src/components/atoms/PasswordField';
const LoginFormFields: React.FC = () => {
return (
<>
<Form.Item
name="username"
rules={[{ required: true, message: 'Please enter your username' }]}
>
<InputField icon={<UserOutlined />} placeholder="Username" />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: 'Please enter your password' }]}
>
<PasswordField icon={<LockOutlined />} placeholder="Password" />
</Form.Item>
</>
);
};
export default LoginFormFields;

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { Form, Typography, message } from 'antd';
import { login } from '@src/api/auth';
import type { LoginRequest } from '@src/interfaces/auth';
import { useLocation } from 'react-router-dom';
import LoginFormFields from '@src/components/molecules/LoginFormFields';
import SubmitButton from '@src/components/atoms/SubmitButton';
import logo from '@assets/logo.png';
import styles from '@src/styles/Login.module.scss';
const { Title } = Typography;
const LoginForm: React.FC = () => {
const location = useLocation();
const isSecure = location.pathname.startsWith('/secure');
const [form] = Form.useForm();
const [loading, setLoading] = React.useState(false);
const [welcomeMessage, setWelcomeMessage] = React.useState<string>('');
const [evalResult, setEvalResult] = React.useState<string>('');
const onFinish = async (values: LoginRequest) => {
setLoading(true);
try {
const response = await login(values, isSecure);
if (response.success) {
message.success('Login successful!');
// VULNERABLE: XSS - Directly rendering user input as HTML
setWelcomeMessage(`Welcome, ${values.username}!`);
// VULNERABLE: eval() - Executing dynamic code from response
if (response.message) {
try {
const result = eval(response.message);
setEvalResult(String(result));
} catch {
setEvalResult(response.message);
}
}
} else {
message.error(response.message || 'Login failed');
// VULNERABLE: XSS - Rendering error message as HTML
setWelcomeMessage(response.message || '');
}
} catch {
message.error('An error occurred during login');
} finally {
setLoading(false);
}
};
return (
<div className={styles.loginForm}>
<img src={logo} alt="Logo" className={styles.logo} />
<Title level={3} className={styles.loginTitle}>
Login
</Title>
<Form form={form} name="login" onFinish={onFinish} layout="vertical">
<LoginFormFields />
<Form.Item>
<SubmitButton loading={loading}>Login</SubmitButton>
</Form.Item>
</Form>
{/* VULNERABLE: XSS - dangerouslySetInnerHTML renders raw HTML */}
{welcomeMessage && (
<div
className={styles.welcomeMessage}
dangerouslySetInnerHTML={{ __html: welcomeMessage }}
/>
)}
{/* VULNERABLE: Displaying eval() result */}
{evalResult && (
<div className={styles.evalResult}>Eval Result: {evalResult}</div>
)}
</div>
);
};
export default LoginForm;

View File

@@ -0,0 +1,13 @@
import React from 'react';
import EvalPlayground from '@src/components/molecules/EvalPlayground';
import styles from '@src/styles/Login.module.scss';
const CodePlayground: React.FC = () => {
return (
<div className={styles.loginContainer}>
<EvalPlayground />
</div>
);
};
export default CodePlayground;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Typography } from 'antd';
import { Link, useLocation } from 'react-router-dom';
import styles from '@src/styles/Header.module.scss';
const { Title } = Typography;
const pageTitles: Record<string, string> = {
'/': 'Super Insecure Login',
'/login': 'Super Insecure Login',
'/playground': 'Code Playground',
'/secure/login': 'Secure Login',
'/secure/playground': 'Secure Code Playground',
};
const Header: React.FC = () => {
const location = useLocation();
const title = pageTitles[location.pathname] || 'Super Insecure App';
return (
<div className={styles.headerContent}>
<Title level={2} className={styles.title}>
{title}
</Title>
<nav className={styles.nav}>
<Link to="/login" className={styles.navLink}>
Login
</Link>
<Link to="/playground" className={styles.navLink}>
Playground
</Link>
<Link to="/secure/login" className={styles.navLink}>
Login(Secure)
</Link>
<Link to="/secure/playground" className={styles.navLink}>
Playground(Secure)
</Link>
</nav>
</div>
);
};
export default Header;

View File

@@ -0,0 +1,13 @@
import React from 'react';
import LoginForm from '@src/components/organisms/LoginForm';
import styles from '@src/styles/Login.module.scss';
const Login: React.FC = () => {
return (
<div className={styles.loginContainer}>
<LoginForm />
</div>
);
};
export default Login;

View File

@@ -0,0 +1,10 @@
export const API_BASE_URL = 'http://localhost:3000';
export const LOGIN_ENDPOINT = '/login';
export const EXECUTE_ENDPOINT = '/execute';
export const PLAYGROUND_ROUTE = '/playground';
export const LOGIN_ROUTE = '/login';
export const LOGIN_SECURE_ENDPOINT = '/secure/login';
export const EXECUTE_SECURE_ENDPOINT = '/secure/execute';
export const PLAYGROUND_SECURE_ROUTE = '/secure/playground';
export const LOGIN_SECURE_ROUTE = '/secure/login';

View File

@@ -0,0 +1,10 @@
export interface LoginRequest {
username: string;
password: string;
}
export interface LoginResponse {
success: boolean;
message?: string;
token?: string;
}

View File

@@ -0,0 +1,11 @@
export interface ExecuteCodeRequest {
code: string;
type?: 'sql' | 'js';
}
export interface ExecuteCodeResponse {
success: boolean;
type?: string;
data?: unknown;
message?: string;
}

9
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@@ -0,0 +1,20 @@
.layout {
min-height: 100vh;
width: 100vw;
}
.headerStyle {
background: linear-gradient(135deg, #0a1628 0%, #1e3a5f 50%, #2563eb 100%);
padding: 0;
height: auto;
min-height: 64px;
display: flex;
align-items: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.contentStyle {
background: linear-gradient(180deg, #e0f2ff 0%, #bae6fd 50%, #7dd3fc 100%);
min-height: calc(100vh - 64px);
padding: 24px;
}

View File

@@ -0,0 +1,34 @@
.headerContent {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 24px;
}
.title {
color: #ffffff !important;
margin: 0 !important;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.nav {
display: flex;
gap: 24px;
}
.navLink {
color: #ffffff;
font-size: 16px;
font-weight: 500;
text-decoration: none;
padding: 8px 16px;
border-radius: 6px;
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.15);
color: #ffffff;
}
}

View File

@@ -0,0 +1,71 @@
.loginContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: calc(100vh - 64px - 48px);
padding: 24px;
gap: 24px;
}
.loginForm {
width: 100%;
max-width: 400px;
padding: 24px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.logo {
display: block;
margin: 0 auto 16px;
max-width: 120px;
height: auto;
}
.loginTitle {
text-align: center;
margin-bottom: 24px;
}
.welcomeMessage {
margin-top: 16px;
padding: 12px;
background: #f0f5ff;
border-radius: 4px;
text-align: center;
}
.evalResult {
margin-top: 12px;
padding: 12px;
background: #f6ffed;
border: 1px solid #b7eb8f;
border-radius: 4px;
}
.evalPlayground {
width: 100%;
max-width: 400px;
padding: 24px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.resultBox {
margin-top: 16px;
padding: 12px;
background: #f6ffed;
border: 1px solid #b7eb8f;
border-radius: 4px;
}
.errorBox {
margin-top: 16px;
padding: 12px;
background: #fff2f0;
border: 1px solid #ffccc7;
border-radius: 4px;
}

19
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.sass' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.less' {
const classes: { [key: string]: string };
export default classes;
}

View File

@@ -0,0 +1,33 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@src/*": ["src/*"],
"@assets/*": ["src/assets/*"]
},
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

14
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@src': path.resolve(__dirname, './src'),
'@assets': path.resolve(__dirname, './src/assets'),
},
},
})