Bruteforce protection & new dockerhub config

This commit is contained in:
abiteman
2025-01-27 22:45:55 -06:00
parent d3e96eee40
commit 87d5987809
3 changed files with 98 additions and 10 deletions

View File

@@ -24,9 +24,9 @@ jobs:
# Step 3: Build the Docker image
- name: Build Docker Image
run: |
docker build -t abite3/dumbdrop:latest .
docker build -t dumbwareio/dumbdrop:latest .
# Step 4: Push the Docker image to Docker Hub
- name: Push Docker Image
run: |
docker push abite3/dumbdrop:latest
docker push dumbwareio/dumbdrop:latest

View File

@@ -41,6 +41,16 @@
.error-message {
text-align: center;
margin-top: 1rem;
color: var(--danger-color);
max-width: 300px;
line-height: 1.4;
}
.pin-digit.locked {
opacity: 0.5;
cursor: not-allowed;
border-color: var(--danger-color);
background-color: var(--textarea-bg);
}
</style>
</head>
@@ -125,13 +135,25 @@
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();
// Show error message
document.getElementById('pin-error').textContent = data.error;
// Only clear inputs and focus if not locked out
if (!data.error.includes('try again in')) {
const inputs = [...document.querySelectorAll('.pin-digit')];
inputs.forEach(input => {
input.value = '';
input.classList.remove('filled');
});
inputs[0].focus();
} else {
// If locked out, disable all inputs
const inputs = [...document.querySelectorAll('.pin-digit')];
inputs.forEach(input => {
input.disabled = true;
input.classList.add('locked');
});
}
}
} catch (err) {
console.error('Error verifying PIN:', err);

View File

@@ -12,6 +12,50 @@ const port = process.env.PORT || 3000;
const uploadDir = './uploads'; // Local development
const maxFileSize = parseInt(process.env.MAX_FILE_SIZE || '1024') * 1024 * 1024; // Convert MB to bytes
// Brute force protection setup
const loginAttempts = new Map(); // Stores IP addresses and their attempt counts
const MAX_ATTEMPTS = 5; // Maximum allowed attempts
const LOCKOUT_TIME = 15 * 60 * 1000; // 15 minutes in milliseconds
// Reset attempts for an IP
function resetAttempts(ip) {
loginAttempts.delete(ip);
}
// Check if an IP is locked out
function isLockedOut(ip) {
const attempts = loginAttempts.get(ip);
if (!attempts) return false;
if (attempts.count >= MAX_ATTEMPTS) {
const timeElapsed = Date.now() - attempts.lastAttempt;
if (timeElapsed < LOCKOUT_TIME) {
return true;
}
resetAttempts(ip);
}
return false;
}
// Record an attempt for an IP
function recordAttempt(ip) {
const attempts = loginAttempts.get(ip) || { count: 0, lastAttempt: 0 };
attempts.count += 1;
attempts.lastAttempt = Date.now();
loginAttempts.set(ip, attempts);
return attempts;
}
// Cleanup old lockouts every minute
setInterval(() => {
const now = Date.now();
for (const [ip, attempts] of loginAttempts.entries()) {
if (now - attempts.lastAttempt >= LOCKOUT_TIME) {
loginAttempts.delete(ip);
}
}
}, 60000);
// Validate and set PIN
const validatePin = (pin) => {
if (!pin) return null;
@@ -78,14 +122,27 @@ function safeCompare(a, b) {
// Pin verification endpoint
app.post('/api/verify-pin', (req, res) => {
const { pin } = req.body;
const ip = req.ip;
// If no PIN is set in env, always return success
if (!PIN) {
return res.json({ success: true });
}
// Check for lockout
if (isLockedOut(ip)) {
const attempts = loginAttempts.get(ip);
const timeLeft = Math.ceil((LOCKOUT_TIME - (Date.now() - attempts.lastAttempt)) / 1000 / 60);
return res.status(429).json({
error: `Too many attempts. Please try again in ${timeLeft} minutes.`
});
}
// Verify the PIN using constant-time comparison
if (safeCompare(pin, PIN)) {
// Reset attempts on successful login
resetAttempts(ip);
// Set secure cookie
res.cookie('DUMBDROP_PIN', pin, {
httpOnly: true,
@@ -95,7 +152,16 @@ app.post('/api/verify-pin', (req, res) => {
});
res.json({ success: true });
} else {
res.status(401).json({ success: false, error: 'Invalid PIN' });
// Record failed attempt
const attempts = recordAttempt(ip);
const attemptsLeft = MAX_ATTEMPTS - attempts.count;
res.status(401).json({
success: false,
error: attemptsLeft > 0 ?
`Invalid PIN. ${attemptsLeft} attempts remaining.` :
'Too many attempts. Account locked for 15 minutes.'
});
}
});