mirror of
https://github.com/DumbWareio/DumbDrop.git
synced 2025-11-03 05:23:39 +00:00
Fixed Security Vulnerability
This commit is contained in:
23
package-lock.json
generated
23
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
@@ -1109,6 +1110,28 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
||||
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"license": "ISC",
|
||||
"description": "A simple file upload application",
|
||||
"dependencies": {
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
|
||||
@@ -9,22 +9,6 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="pin-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="pin-header">
|
||||
<h1>DumbDrop</h1>
|
||||
<h2>Enter PIN</h2>
|
||||
</div>
|
||||
<div id="pin-container" class="pin-container">
|
||||
<!-- PIN inputs will be dynamically added here -->
|
||||
</div>
|
||||
<p id="pin-error" class="error-message"></p>
|
||||
<div class="modal-buttons">
|
||||
<button id="pin-submit" class="primary">Verify PIN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="theme-toggle-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -428,166 +412,6 @@
|
||||
const savedTheme = localStorage.getItem('theme') ||
|
||||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
setTheme(savedTheme);
|
||||
|
||||
let verifiedPin = null;
|
||||
let pinDigits = null; // Declare pinDigits at a higher scope
|
||||
|
||||
// Create PIN input boxes
|
||||
const createPinInputs = (length) => {
|
||||
const container = document.getElementById('pin-container');
|
||||
container.innerHTML = ''; // Clear existing inputs
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'pin-digit';
|
||||
input.maxLength = 1;
|
||||
input.pattern = '[0-9]';
|
||||
input.inputMode = 'numeric';
|
||||
input.autocomplete = 'off';
|
||||
container.appendChild(input);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if PIN is required
|
||||
const checkPinRequired = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/pin-required');
|
||||
const { required, length } = await response.json();
|
||||
if (required) {
|
||||
document.getElementById('pin-modal').classList.add('visible');
|
||||
createPinInputs(length); // Create the required number of input boxes
|
||||
setupPinHandling();
|
||||
} else {
|
||||
initializeApp();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error checking PIN requirement:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle PIN input
|
||||
const handlePinInput = (e, index) => {
|
||||
const input = e.target;
|
||||
const value = input.value;
|
||||
|
||||
// Only allow numbers
|
||||
if (!/^\d*$/.test(value)) {
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
input.classList.add('filled');
|
||||
// Move to next input if available
|
||||
if (index < pinDigits.length - 1) {
|
||||
pinDigits[index + 1].focus();
|
||||
} else {
|
||||
// If this is the last digit, submit automatically
|
||||
const pin = Array.from(pinDigits).map(digit => digit.value).join('');
|
||||
if (pin.length === pinDigits.length) {
|
||||
verifyPin(pin);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
input.classList.remove('filled');
|
||||
}
|
||||
};
|
||||
|
||||
// Verify PIN
|
||||
const verifyPin = async (pin) => {
|
||||
try {
|
||||
const response = await fetch('/api/verify-pin', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pin })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
verifiedPin = pin;
|
||||
document.getElementById('pin-modal').classList.remove('visible');
|
||||
initializeApp();
|
||||
} else {
|
||||
document.getElementById('pin-error').textContent = 'Invalid PIN';
|
||||
pinDigits.forEach(digit => {
|
||||
digit.value = '';
|
||||
digit.classList.remove('filled');
|
||||
});
|
||||
pinDigits[0].focus();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error verifying PIN:', err);
|
||||
document.getElementById('pin-error').textContent = 'Error verifying PIN';
|
||||
}
|
||||
};
|
||||
|
||||
// Setup PIN handling
|
||||
const setupPinHandling = () => {
|
||||
pinDigits = Array.from(document.querySelectorAll('.pin-digit')); // Assign to the outer variable
|
||||
const submitButton = document.getElementById('pin-submit');
|
||||
|
||||
pinDigits.forEach((input, index) => {
|
||||
input.addEventListener('input', (e) => handlePinInput(e, index));
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Backspace' && !input.value && index > 0) {
|
||||
pinDigits[index - 1].focus();
|
||||
pinDigits[index - 1].value = '';
|
||||
pinDigits[index - 1].classList.remove('filled');
|
||||
}
|
||||
});
|
||||
// Handle paste event
|
||||
input.addEventListener('paste', (e) => {
|
||||
e.preventDefault();
|
||||
const pastedData = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, pinDigits.length);
|
||||
if (pastedData.length > 0) {
|
||||
pastedData.split('').forEach((digit, i) => {
|
||||
if (i < pinDigits.length) {
|
||||
pinDigits[i].value = digit;
|
||||
pinDigits[i].classList.add('filled');
|
||||
}
|
||||
});
|
||||
if (pastedData.length === pinDigits.length) {
|
||||
verifyPin(pastedData);
|
||||
} else if (pastedData.length < pinDigits.length) {
|
||||
pinDigits[pastedData.length].focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
submitButton.addEventListener('click', () => {
|
||||
const pin = pinDigits.map(digit => digit.value).join('');
|
||||
if (pin.length === pinDigits.length) {
|
||||
verifyPin(pin);
|
||||
}
|
||||
});
|
||||
|
||||
// Focus first input
|
||||
pinDigits[0].focus();
|
||||
};
|
||||
|
||||
// Add PIN to all API requests
|
||||
const fetchWithPin = async (url, options = {}) => {
|
||||
if (verifiedPin) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
'X-Pin': verifiedPin
|
||||
};
|
||||
}
|
||||
return window.originalFetch(url, options); // Use originalFetch instead of fetch
|
||||
};
|
||||
|
||||
// Initialize the app
|
||||
const initializeApp = () => {
|
||||
// Store original fetch
|
||||
window.originalFetch = window.fetch;
|
||||
// Replace fetch with our version
|
||||
window.fetch = fetchWithPin;
|
||||
};
|
||||
|
||||
// Start the app by checking if PIN is required
|
||||
checkPinRequired();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
206
public/login.html
Normal file
206
public/login.html
Normal file
@@ -0,0 +1,206 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DumbDrop - Login</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
.login-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.pin-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.pin-header h1 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pin-header h2 {
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#pin-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="pin-header">
|
||||
<h1>DumbDrop</h1>
|
||||
<h2>Enter PIN</h2>
|
||||
</div>
|
||||
<form id="pin-form">
|
||||
<!-- PIN inputs will be dynamically added here -->
|
||||
</form>
|
||||
<p id="pin-error" class="error-message"></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize theme
|
||||
const savedTheme = localStorage.getItem('theme') ||
|
||||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
|
||||
let pinLength = 4; // Default length, will be updated from server
|
||||
|
||||
// Create PIN input boxes
|
||||
const createPinInputs = (length) => {
|
||||
const form = document.getElementById('pin-form');
|
||||
form.innerHTML = ''; // Clear existing inputs
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'pin-digit';
|
||||
input.maxLength = 1;
|
||||
input.pattern = '[0-9]';
|
||||
input.inputMode = 'numeric';
|
||||
input.autocomplete = 'off';
|
||||
input.required = true;
|
||||
form.appendChild(input);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle PIN input
|
||||
const handlePinInput = (e, index) => {
|
||||
const input = e.target;
|
||||
const form = document.getElementById('pin-form');
|
||||
const inputs = [...form.querySelectorAll('.pin-digit')];
|
||||
const value = input.value;
|
||||
|
||||
// Only allow numbers
|
||||
if (!/^\d*$/.test(value)) {
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
input.classList.add('filled');
|
||||
// Move to next input if available or submit if last
|
||||
if (index < inputs.length - 1) {
|
||||
inputs[index + 1].focus();
|
||||
} else {
|
||||
// Auto-submit when last digit is entered
|
||||
const pin = inputs.map(input => input.value).join('');
|
||||
if (pin.length === inputs.length) {
|
||||
verifyPin(pin);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
input.classList.remove('filled');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
const verifyPin = async (pin) => {
|
||||
try {
|
||||
const response = await fetch('/api/verify-pin', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pin })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
document.getElementById('pin-error').textContent = 'Invalid PIN';
|
||||
const inputs = [...document.querySelectorAll('.pin-digit')];
|
||||
inputs.forEach(input => {
|
||||
input.value = '';
|
||||
input.classList.remove('filled');
|
||||
});
|
||||
inputs[0].focus();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error verifying PIN:', err);
|
||||
document.getElementById('pin-error').textContent = 'Error verifying PIN';
|
||||
}
|
||||
};
|
||||
|
||||
// Setup PIN handling
|
||||
const setupPinHandling = () => {
|
||||
const form = document.getElementById('pin-form');
|
||||
const inputs = [...form.querySelectorAll('.pin-digit')];
|
||||
|
||||
inputs.forEach((input, index) => {
|
||||
input.addEventListener('input', (e) => handlePinInput(e, index));
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Backspace' && !input.value && index > 0) {
|
||||
inputs[index - 1].focus();
|
||||
inputs[index - 1].value = '';
|
||||
inputs[index - 1].classList.remove('filled');
|
||||
}
|
||||
});
|
||||
// Handle paste event
|
||||
input.addEventListener('paste', (e) => {
|
||||
e.preventDefault();
|
||||
const pastedData = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, inputs.length);
|
||||
if (pastedData.length > 0) {
|
||||
pastedData.split('').forEach((digit, i) => {
|
||||
if (i < inputs.length) {
|
||||
inputs[i].value = digit;
|
||||
inputs[i].classList.add('filled');
|
||||
}
|
||||
});
|
||||
if (pastedData.length === inputs.length) {
|
||||
verifyPin(pastedData);
|
||||
} else if (pastedData.length < inputs.length) {
|
||||
inputs[pastedData.length].focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const pin = inputs.map(input => input.value).join('');
|
||||
if (pin.length === inputs.length) {
|
||||
verifyPin(pin);
|
||||
}
|
||||
});
|
||||
|
||||
// Focus first input
|
||||
inputs[0].focus();
|
||||
};
|
||||
|
||||
// Check PIN length and initialize
|
||||
fetch('/api/pin-required')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.required) {
|
||||
pinLength = data.length;
|
||||
createPinInputs(pinLength);
|
||||
setupPinHandling();
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error checking PIN requirement:', err);
|
||||
document.getElementById('pin-error').textContent = 'Error checking PIN requirement';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
46
server.js
46
server.js
@@ -4,6 +4,7 @@ const path = require('path');
|
||||
const cors = require('cors');
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const cookieParser = require('cookie-parser');
|
||||
require('dotenv').config();
|
||||
|
||||
const app = express();
|
||||
@@ -58,7 +59,7 @@ try {
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.static('public'));
|
||||
app.use(cookieParser());
|
||||
app.use(express.json());
|
||||
|
||||
// Helper function for constant-time string comparison
|
||||
@@ -85,6 +86,13 @@ app.post('/api/verify-pin', (req, res) => {
|
||||
|
||||
// Verify the PIN using constant-time comparison
|
||||
if (safeCompare(pin, PIN)) {
|
||||
// Set secure cookie
|
||||
res.cookie('DUMBDROP_PIN', pin, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
path: '/'
|
||||
});
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(401).json({ success: false, error: 'Invalid PIN' });
|
||||
@@ -105,7 +113,7 @@ const requirePin = (req, res, next) => {
|
||||
return next();
|
||||
}
|
||||
|
||||
const providedPin = req.headers['x-pin'];
|
||||
const providedPin = req.headers['x-pin'] || req.cookies.DUMBDROP_PIN;
|
||||
if (!safeCompare(providedPin, PIN)) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
@@ -115,6 +123,40 @@ const requirePin = (req, res, next) => {
|
||||
// Apply pin protection to all /upload routes
|
||||
app.use('/upload', requirePin);
|
||||
|
||||
// Serve login page and its assets without PIN check
|
||||
app.use((req, res, next) => {
|
||||
if (req.path === '/login.html' || req.path === '/styles.css' || req.path.startsWith('/api/')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check PIN requirement
|
||||
if (!PIN) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check cookie
|
||||
const providedPin = req.cookies.DUMBDROP_PIN;
|
||||
if (!safeCompare(providedPin, PIN)) {
|
||||
// If requesting HTML or root, redirect to login
|
||||
if (req.path === '/' || req.path.endsWith('.html')) {
|
||||
return res.redirect('/login.html');
|
||||
}
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Serve static files
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Handle root route
|
||||
app.get('/', (req, res) => {
|
||||
if (PIN && !safeCompare(req.cookies.DUMBDROP_PIN, PIN)) {
|
||||
return res.redirect('/login.html');
|
||||
}
|
||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
});
|
||||
|
||||
// Store ongoing uploads
|
||||
const uploads = new Map();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user