Diff
381 lines
// A simple Express.js application to demonstrate user registration, login,
// A simple Express.js application to demonstrate user registration, login,
// session handling, and safe rendering of user-controlled data.
// session handling, and safe rendering of user-controlled data.
//
//
// === WARNING: FOR CTF (CAPTURE THE FLAG) USE ONLY ===
// === WARNING: FOR CTF (CAPTURE THE FLAG) USE ONLY ===
// This code stores passwords in plaintext. This is a severe security vulnerability
// This code stores passwords in plaintext. This is a severe security vulnerability
// and should NEVER be used in a production or real-world application.
// and should NEVER be used in a production or real-world application.
//
//
// This is done to fulfill the user's request for a CTF scenario.
// This is done to fulfill the user's request for a CTF scenario.
// 1. Import necessary libraries
// 1. Import necessary libraries
// Note: You will need to run `npm install express express-session` to use this code.
// Note: You will need to run `npm install express express-session` to use this code.
// bcrypt has been removed as per the CTF request.
// bcrypt has been removed as per the CTF request.
const express = require('express');
const express = require('express');
const session = require('express-session');
const session = require('express-session');
const crypto = require('crypto');
const app = express();
const app = express();
const PORT = 3000;
const PORT = 3000;
const FLAG = process.env.FLAG || "justCTF{example_flag}"
const SECRET = crypto.randomBytes(24).toString('hex');
// 2. Middleware to parse request bodies (for form data)
// 2. Middleware to parse request bodies (for form data)
app.use(express.urlencoded({ extended: true }));
app.use(express.urlencoded({ extended: true }));
// 3. Configure session middleware
// 3. Configure session middleware
app.use(session({
app.use(session({
secret: 'a_very_secure_secret_key_for_session', // In a real app, this should be an environment variable
secret: SECRET,
resave: false,
resave: false,
saveUninitialized: true,
saveUninitialized: true,
cookie: { secure: false } // Set to true if using HTTPS
cookie: { secure: false } // Set to true if using HTTPS
}));
}));
// 4. In-memory user data store (simulating a database)
// 4. In-memory user data store (simulating a database)
// In a real application, you would use a database like MongoDB or PostgreSQL.
// In a real application, you would use a database like MongoDB or PostgreSQL.
// For this CTF scenario, passwords are stored in plaintext.
// For this CTF scenario, passwords are stored in plaintext.
const users = {}; // Stores { username: { password, userThemeConfig } }
const users = {}; // Stores { username: { password, userThemeConfig, isAdmin } }
// 5. A simple function to safely escape HTML to prevent XSS attacks.
// 5. A simple function to safely escape HTML to prevent XSS attacks.
const escapeHtml = (unsafe) => {
const escapeHtml = (unsafe) => {
if (typeof unsafe !== 'string') return unsafe;
if (typeof unsafe !== 'string') return unsafe;
return unsafe
return unsafe
.replace(/&/g, "&")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/"/g, """)
.replace(/'/g, "'");
.replace(/'/g, "'");
};
};
// 6. A function to recursively merge objects
// 6. A function to recursively merge objects
const deepMerge = (target, source) => {
const deepMerge = (target, source) => {
for (const key in source) {
for (const key in source) {
if (source[key] instanceof Object && key in target) {
if (source[key] instanceof Object && key in target) {
Object.assign(source[key], deepMerge(target[key], source[key]));
Object.assign(source[key], deepMerge(target[key], source[key]));
}
}
}
}
Object.assign(target || {}, source);
Object.assign(target || {}, source);
return target;
return target;
};
};
// 7. A function to parse a query string with dot-notation keys.
// 7. A function to parse a query string with dot-notation keys.
const parseQueryParams = (queryString) => {
const parseQueryParams = (queryString) => {
if (typeof queryString !== 'string') {
if (typeof queryString !== 'string') {
return {};
return {};
}
}
const cleanString = queryString.startsWith('?') ? queryString.substring(1) : queryString;
const cleanString = queryString.startsWith('?') ? queryString.substring(1) : queryString;
const params = new URLSearchParams(cleanString);
const params = new URLSearchParams(cleanString);
const result = {};
const result = {};
for (const [key, value] of params.entries()) {
for (const [key, value] of params.entries()) {
const path = key.split('.');
const path = key.split('.');
let current = result;
let current = result;
for (let i = 0; i < path.length; i++) {
for (let i = 0; i < path.length; i++) {
const part = path[i];
let part = path[i];
// Protect against Prototype Pollution vulnerability
if(['__proto__', 'prototype', 'constructor'].includes(part)){
part = '__unsafe$' + part;
}
if (i === path.length - 1) {
if (i === path.length - 1) {
current[part] = value;
current[part] = value;
} else {
} else {
if (!current[part] || typeof current[part] !== 'object') {
if (!current[part] || typeof current[part] !== 'object') {
current[part] = {};
current[part] = {};
}
}
current = current[part];
current = current[part];
}
}
}
}
}
}
return result;
return result;
};
};
// 8. Authentication Middleware
// 8. Authentication Middleware
// This function checks if a user is logged in before allowing access to a route.
// This function checks if a user is logged in before allowing access to a route.
const isAuthenticated = (req, res, next) => {
const isAuthenticated = (req, res, next) => {
if (req.session.userId) {
if (req.session.userId) {
next(); // User is authenticated, proceed to the next middleware/route handler
next(); // User is authenticated, proceed to the next middleware/route handler
} else {
} else {
res.redirect('/login'); // User is not authenticated, redirect to login page
res.redirect('/login'); // User is not authenticated, redirect to login page
}
}
};
};
// 9. Default Theme Configuration
// 9. Default Theme Configuration
const defaultThemeConfig = {
const defaultThemeConfig = {
theme: {
theme: {
primaryColor: '#8E24AA', // A nice shade of purple
primaryColor: '#8E24AA', // A nice shade of purple
secondaryColor: '#FFC107', // An amber yellow
secondaryColor: '#FFC107', // An amber yellow
fontSize: '18px',
fontSize: '18px',
fontFamily: 'Arial, sans-serif'
fontFamily: 'Arial, sans-serif'
}
}
};
};
// 10. Helper function to generate a styled HTML page
// 10. Helper function to generate a styled HTML page
const generateThemedPage = (pageBody, themeConfig, title = 'Theme Configuration App') => {
const generateThemedPage = (pageBody, themeConfig, title = 'Theme Configuration App') => {
return `
return `
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<html lang="en">
<head>
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${escapeHtml(title)}</title>
<title>${escapeHtml(title)}</title>
<style>
<style>
body {
body {
font-family: ${escapeHtml(themeConfig.theme.fontFamily)};
font-family: ${escapeHtml(themeConfig.theme.fontFamily)};
background-color: #121212;
background-color: #121212;
color: #E0E0E0;
color: #E0E0E0;
display: flex;
display: flex;
justify-content: center;
justify-content: center;
align-items: center;
align-items: center;
min-height: 100vh;
min-height: 100vh;
margin: 0;
margin: 0;
padding: 20px;
padding: 20px;
flex-direction: column;
flex-direction: column;
gap: 20px;
gap: 20px;
}
}
.container {
.container {
max-width: 800px;
max-width: 800px;
padding: 40px;
padding: 40px;
border-radius: 10px;
border-radius: 10px;
background-color: #1E1E1E;
background-color: #1E1E1E;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
text-align: center;
text-align: center;
width: 100%;
width: 100%;
}
}
.form-container {
.form-container {
max-width: 800px;
max-width: 800px;
padding: 20px;
padding: 20px;
border-radius: 10px;
border-radius: 10px;
background-color: #1E1E1E;
background-color: #1E1E1E;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
width: 100%;
width: 100%;
}
}
h1 {
h1 {
color: ${escapeHtml(themeConfig.theme.primaryColor)};
color: ${escapeHtml(themeConfig.theme.primaryColor)};
font-size: 2.5rem;
font-size: 2.5rem;
margin-bottom: 0.5rem;
margin-bottom: 0.5rem;
}
}
p {
p {
font-size: ${escapeHtml(themeConfig.theme.fontSize)};
font-size: ${escapeHtml(themeConfig.theme.fontSize)};
line-height: 1.6;
line-height: 1.6;
}
}
a {
a {
color: ${escapeHtml(themeConfig.theme.primaryColor)};
color: ${escapeHtml(themeConfig.theme.primaryColor)};
text-decoration: none;
text-decoration: none;
font-weight: bold;
font-weight: bold;
transition: color 0.3s ease;
transition: color 0.3s ease;
}
}
a:hover {
a:hover {
text-decoration: underline;
text-decoration: underline;
color: ${escapeHtml(themeConfig.theme.secondaryColor)};
color: ${escapeHtml(themeConfig.theme.secondaryColor)};
}
}
pre {
pre {
background-color: #000000;
background-color: #000000;
color: #00FF00;
color: #00FF00;
padding: 20px;
padding: 20px;
border-radius: 8px;
border-radius: 8px;
border: 1px solid ${escapeHtml(themeConfig.theme.secondaryColor)};
border: 1px solid ${escapeHtml(themeConfig.theme.secondaryColor)};
overflow-x: auto;
overflow-x: auto;
text-align: left;
text-align: left;
}
}
form {
form {
display: flex;
display: flex;
flex-direction: column;
flex-direction: column;
gap: 15px;
gap: 15px;
text-align: left;
text-align: left;
}
}
label {
label {
font-weight: bold;
font-weight: bold;
color: #E0E0E0;
color: #E0E0E0;
}
}
input[type="text"], input[type="password"], input[type="color"] {
input[type="text"], input[type="password"], input[type="color"] {
width: 100%;
width: 100%;
padding: 8px;
padding: 8px;
border-radius: 5px;
border-radius: 5px;
border: 1px solid ${escapeHtml(themeConfig.theme.primaryColor)};
border: 1px solid ${escapeHtml(themeConfig.theme.primaryColor)};
background-color: #2D2D2D;
background-color: #2D2D2D;
color: #E0E0E0;
color: #E0E0E0;
box-sizing: border-box;
box-sizing: border-box;
}
}
input[type="color"] {
input[type="color"] {
padding: 0;
padding: 0;
height: 40px;
height: 40px;
}
}
button {
button {
background-color: ${escapeHtml(themeConfig.theme.primaryColor)};
background-color: ${escapeHtml(themeConfig.theme.primaryColor)};
color: #fff;
color: #fff;
border: none;
border: none;
padding: 12px 20px;
padding: 12px 20px;
border-radius: 5px;
border-radius: 5px;
cursor: pointer;
cursor: pointer;
font-size: 1rem;
font-size: 1rem;
font-weight: bold;
font-weight: bold;
transition: background-color 0.3s ease;
transition: background-color 0.3s ease;
}
}
button:hover {
button:hover {
background-color: ${escapeHtml(themeConfig.theme.secondaryColor)};
background-color: ${escapeHtml(themeConfig.theme.secondaryColor)};
}
}
.error-message {
.error-message {
color: #FF6B6B;
color: #FF6B6B;
font-size: 0.9rem;
font-size: 0.9rem;
text-align: center;
text-align: center;
}
}
</style>
</style>
</head>
</head>
<body>
<body>
${pageBody}
${pageBody}
</body>
</body>
</html>
</html>
`;
`;
};
};
// 11. Registration Routes
// 11. Registration Routes
app.get('/register', (req, res) => {
app.get('/register', (req, res) => {
const errorMessage = req.session.errorMessage;
const errorMessage = req.session.errorMessage;
req.session.errorMessage = null; // Clear the error message after displaying it
req.session.errorMessage = null; // Clear the error message after displaying it
const errorHtml = errorMessage ? `<p class="error-message">${escapeHtml(errorMessage)}</p>` : '';
const errorHtml = errorMessage ? `<p class="error-message">${escapeHtml(errorMessage)}</p>` : '';
const pageBody = `
const pageBody = `
<div class="container">
<div class="container">
<h1>Register</h1>
<h1>Register</h1>
${errorHtml}
${errorHtml}
<form action="/register" method="POST">
<form action="/register" method="POST">
<input type="text" name="username" placeholder="Username" required><br><br>
<input type="text" name="username" placeholder="Username" required><br><br>
<input type="password" name="password" placeholder="Password" required><br><br>
<input type="password" name="password" placeholder="Password" required><br><br>
<button type="submit">Register</button>
<button type="submit">Register</button>
</form>
</form>
<p>Already have an account? <a href="/login">Login here</a></p>
<p>Already have an account? <a href="/login">Login here</a></p>
</div>
</div>
`;
`;
res.send(generateThemedPage(pageBody, defaultThemeConfig, 'Register'));
res.send(generateThemedPage(pageBody, defaultThemeConfig, 'Register'));
});
});
app.post('/register', (req, res) => {
app.post('/register', (req, res) => {
const { username, password } = req.body;
const { username, password } = req.body;
if (users[username]) {
if (users[username]) {
req.session.errorMessage = 'User already exists!';
req.session.errorMessage = 'User already exists!';
return res.redirect('/register');
return res.redirect('/register');
}
}
// Storing the password in plaintext for the CTF scenario.
// Storing the password in plaintext for the CTF scenario.
// DO NOT do this in a real application!
// DO NOT do this in a real application!
users[username] = {
users[username] = {
password: password,
password: password,
isAdmin: false,
themeConfig: {
themeConfig: {
theme: {
theme: {
primaryColor: '#6200EE',
primaryColor: '#6200EE',
secondaryColor: '#03DAC6',
secondaryColor: '#03DAC6',
fontSize: '16px',
fontSize: '16px',
fontFamily: 'Roboto, sans-serif'
fontFamily: 'Roboto, sans-serif'
}
}
}
}
};
};
req.session.userId = username;
req.session.userId = username;
res.redirect('/');
res.redirect('/');
});
});
// 12. Login Routes
// 12. Login Routes
app.get('/login', (req, res) => {
app.get('/login', (req, res) => {
const errorMessage = req.session.errorMessage;
const errorMessage = req.session.errorMessage;
req.session.errorMessage = null; // Clear the error message after displaying it
req.session.errorMessage = null; // Clear the error message after displaying it
const errorHtml = errorMessage ? `<p class="error-message">${escapeHtml(errorMessage)}</p>` : '';
const errorHtml = errorMessage ? `<p class="error-message">${escapeHtml(errorMessage)}</p>` : '';
const pageBody = `
const pageBody = `
<div class="container">
<div class="container">
<h1>Login</h1>
<h1>Login</h1>
${errorHtml}
${errorHtml}
<form action="/login" method="POST">
<form action="/login" method="POST">
<input type="text" name="username" placeholder="Username" required><br><br>
<input type="text" name="username" placeholder="Username" required><br><br>
<input type="password" name="password" placeholder="Password" required><br><br>
<input type="password" name="password" placeholder="Password" required><br><br>
<button type="submit">Login</button>
<button type="submit">Login</button>
</form>
</form>
<p>Don't have an account? <a href="/register">Register here</a></p>
<p>Don't have an account? <a href="/register">Register here</a></p>
</div>
</div>
`;
`;
res.send(generateThemedPage(pageBody, defaultThemeConfig, 'Login'));
res.send(generateThemedPage(pageBody, defaultThemeConfig, 'Login'));
});
});
app.post('/login', (req, res) => {
app.post('/login', (req, res) => {
const { username, password } = req.body;
const { username, password } = req.body;
const user = users[username];
const user = users[username];
// Comparing the plaintext password for the CTF scenario.
// Comparing the plaintext password for the CTF scenario.
// DO NOT do this in a real application!
// DO NOT do this in a real application!
if (user && user.password === password) {
if (user && user.password === password) {
req.session.userId = username;
req.session.userId = username;
res.redirect('/');
res.redirect('/');
} else {
} else {
req.session.errorMessage = 'Invalid username or password';
req.session.errorMessage = 'Invalid username or password';
res.redirect('/login');
res.redirect('/login');
}
}
});
});
// 13. Logout Route
// 13. Logout Route
app.get('/logout', (req, res) => {
app.get('/logout', (req, res) => {
req.session.destroy(err => {
req.session.destroy(err => {
if (err) {
if (err) {
return res.status(500).send('Could not log out.');
return res.status(500).send('Could not log out.');
}
}
res.redirect('/login');
res.redirect('/login');
});
});
});
});
// 14. Define the root endpoint (protected)
// 14. Define the root endpoint (protected)
app.get('/', isAuthenticated, (req, res) => {
app.get('/', isAuthenticated, (req, res) => {
const user = users[req.session.userId];
const user = users[req.session.userId];
if (!user) {
if (!user) {
return res.redirect('/login');
return res.redirect('/login');
}
}
const themeConfig = user.themeConfig;
const themeConfig = user.themeConfig;
const pageBody = `
const pageBody = `
<div class="container">
<div class="container">
<h1>Welcome, ${escapeHtml(req.session.userId)}!</h1>
<h1>Welcome, ${escapeHtml(req.session.userId)}!</h1>
<p>Current Theme Configuration:</p>
<p>Current Theme Configuration:</p>
<pre>${JSON.stringify(themeConfig, null, 2)}</pre>
<pre>${escapeHtml(JSON.stringify(themeConfig, null, 2))}</pre>
<p><a href="/logout">Logout</a></p>
<p><a href="/logout">Logout</a></p>
</div>
</div>
<div class="form-container">
<div class="form-container">
<h2>Customize Theme</h2>
<h2>Customize Theme</h2>
<form action="/theme" method="GET">
<form action="/theme" method="GET">
<label for="primaryColor">Primary Color:</label>
<label for="primaryColor">Primary Color:</label>
<input type="color" id="primaryColor" name="theme.primaryColor" value="${escapeHtml(themeConfig.theme.primaryColor)}">
<input type="color" id="primaryColor" name="theme.primaryColor" value="${escapeHtml(themeConfig.theme.primaryColor)}">
<label for="secondaryColor">Secondary Color:</label>
<label for="secondaryColor">Secondary Color:</label>
<input type="color" id="secondaryColor" name="theme.secondaryColor" value="${escapeHtml(themeConfig.theme.secondaryColor)}">
<input type="color" id="secondaryColor" name="theme.secondaryColor" value="${escapeHtml(themeConfig.theme.secondaryColor)}">
<label for="fontSize">Font Size (e.g., '16px'):</label>
<label for="fontSize">Font Size (e.g., '16px'):</label>
<input type="text" id="fontSize" name="theme.fontSize" value="${escapeHtml(themeConfig.theme.fontSize)}">
<input type="text" id="fontSize" name="theme.fontSize" value="${escapeHtml(themeConfig.theme.fontSize)}">
<label for="fontFamily">Font Family (e.g., 'Roboto, sans-serif'):</label>
<label for="fontFamily">Font Family (e.g., 'Roboto, sans-serif'):</label>
<input type="text" id="fontFamily" name="theme.fontFamily" value="${escapeHtml(themeConfig.theme.fontFamily)}">
<input type="text" id="fontFamily" name="theme.fontFamily" value="${escapeHtml(themeConfig.theme.fontFamily)}">
<button type="submit">Update Theme</button>
<button type="submit">Update Theme</button>
</form>
</form>
</div>
</div>
`;
`;
res.send(generateThemedPage(pageBody, themeConfig));
res.send(generateThemedPage(pageBody, themeConfig));
});
});
// 15. Define the `/theme` endpoint (protected)
// 15. Define the `/theme` endpoint (protected)
app.get('/theme', isAuthenticated, (req, res) => {
app.get('/theme', isAuthenticated, (req, res) => {
const user = users[req.session.userId];
const user = users[req.session.userId];
if (!user) {
if (!user) {
// This case should be handled by isAuthenticated middleware, but is here as a fallback
// This case should be handled by isAuthenticated middleware, but is here as a fallback
return res.redirect('/login');
return res.redirect('/login');
}
}
// Parse the query string into a nested object
// Parse the query string into a nested object
const queryString = req.url.split('?')[1] || '';
const queryString = req.url.split('?')[1] || '';
const parsedUpdates = parseQueryParams(queryString);
const parsedUpdates = parseQueryParams(queryString);
// If there are updates, merge them into the existing config.
// If there are updates, merge them into the existing config.
if (Object.keys(parsedUpdates).length > 0) {
if (Object.keys(parsedUpdates).length > 0) {
// Merge the parsed updates into the user's theme config.
// Merge the parsed updates into the user's theme config.
user.themeConfig = deepMerge(user.themeConfig, parsedUpdates);
user.themeConfig = deepMerge(user.themeConfig, parsedUpdates);
}
}
// Redirect the user back to the home page to see the updated theme.
// Redirect the user back to the home page to see the updated theme.
res.redirect('/');
res.redirect('/');
});
});
// 15. Define the `/flag` endpoint (protected)
app.get('/flag', isAuthenticated, (req, res, next)=>{
if(users[req.session.userId].isAdmin == true){
return res.end(FLAG);
}
return res.end("Not admin :(");
});
// 16. Start the Express server
// 16. Start the Express server
app.listen(PORT, () => {
app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`);
console.log(`Server is running at http://localhost:${PORT}`);
console.log('Please register or login at http://localhost:3000/register or http://localhost:3000/login');
console.log('Please register or login at http://localhost:3000/register or http://localhost:3000/login');
});
});