mirror of
https://github.com/sassanix/Warracker.git
synced 2026-01-06 05:29:39 -06:00
Security Enhancements
Refer to changelog.md
This commit is contained in:
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [0.9.7.6] - 2025-03-21
|
||||
- **Security Enhancements**
|
||||
- **Secure File Access System**
|
||||
- Implemented comprehensive security controls for uploaded files (invoices, manuals)
|
||||
- Added two secure file serving endpoints with authentication and authorization
|
||||
- Created client-side utility functions for secure file handling
|
||||
- Blocked direct access to the uploads directory via nginx configuration
|
||||
- Added ownership verification to ensure users can only access their own files
|
||||
- Implemented protection against path traversal attacks
|
||||
- Enhanced logging for all file access attempts
|
||||
- **Frontend Security Integration**
|
||||
- Created new file-utils.js with secureFilePath and openSecureFile functions
|
||||
- Updated all file links to use secure handling methods
|
||||
- Added proper authentication token handling for file downloads
|
||||
|
||||
## [0.9.7.5] - 2025-03-14
|
||||
- **Fixes**
|
||||
- Removed remember me
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -93,16 +93,7 @@ RUN echo 'server {\n\
|
||||
\n\
|
||||
# Direct access to uploads directory\n\
|
||||
location /uploads/ {\n\
|
||||
alias /data/uploads/;\n\
|
||||
autoindex on;\n\
|
||||
\n\
|
||||
# Set appropriate MIME types\n\
|
||||
types {\n\
|
||||
image/png png PNG;\n\
|
||||
image/jpeg jpg jpeg JPG JPEG;\n\
|
||||
application/pdf pdf PDF;\n\
|
||||
text/plain txt TXT;\n\
|
||||
}\n\
|
||||
return 403 "Access forbidden";\n\
|
||||
}\n\
|
||||
}' > /etc/nginx/conf.d/default.conf
|
||||
|
||||
|
||||
@@ -260,13 +260,19 @@ def token_required(f):
|
||||
if auth_header and auth_header.startswith('Bearer '):
|
||||
token = auth_header.split(' ')[1]
|
||||
|
||||
# If no token in header, check form data for POST requests
|
||||
if not token and request.method == 'POST':
|
||||
token = request.form.get('auth_token') # Check form data
|
||||
|
||||
# If no token is provided
|
||||
if not token:
|
||||
logger.warning(f"Authentication attempt without token: {request.path}")
|
||||
return jsonify({'message': 'Authentication token is missing!'}), 401
|
||||
|
||||
# Decode the token
|
||||
user_id = decode_token(token)
|
||||
if not user_id:
|
||||
logger.warning(f"Invalid token used for: {request.path}")
|
||||
return jsonify({'message': 'Invalid or expired token!'}), 401
|
||||
|
||||
# Check if user exists
|
||||
@@ -1803,6 +1809,75 @@ def check_registration_status():
|
||||
if conn:
|
||||
release_db_connection(conn)
|
||||
|
||||
# File serving endpoints
|
||||
@app.route('/api/files/<path:filename>', methods=['GET', 'POST'])
|
||||
@token_required
|
||||
def serve_file(filename):
|
||||
"""Basic secure file serving with authentication."""
|
||||
try:
|
||||
logger.info(f"File access request for {filename} by user {request.user['id']}")
|
||||
|
||||
if not filename.startswith('uploads/'):
|
||||
logger.warning(f"Attempted access to non-uploads file: {filename}")
|
||||
return jsonify({"message": "Access denied"}), 403
|
||||
|
||||
# Remove 'uploads/' prefix for send_from_directory
|
||||
file_path = filename[8:] if filename.startswith('uploads/') else filename
|
||||
|
||||
return send_from_directory('/data/uploads', file_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Error serving file {filename}: {e}")
|
||||
return jsonify({"message": "Error accessing file"}), 500
|
||||
|
||||
@app.route('/api/secure-file/<path:filename>', methods=['GET', 'POST'])
|
||||
@token_required
|
||||
def secure_file_access(filename):
|
||||
"""Enhanced secure file serving with authorization checks."""
|
||||
try:
|
||||
logger.info(f"Secure file access request for {filename} by user {request.user['id']}")
|
||||
|
||||
# Security check for path traversal
|
||||
if '..' in filename or filename.startswith('/'):
|
||||
logger.warning(f"Potential path traversal attempt detected: {filename} by user {request.user['id']}")
|
||||
return jsonify({"message": "Invalid file path"}), 400
|
||||
|
||||
# Check if user is authorized to access this file
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# Find warranties that reference this file
|
||||
query = """
|
||||
SELECT w.id, w.user_id
|
||||
FROM warranties w
|
||||
WHERE w.invoice_path = %s OR w.manual_path = %s
|
||||
"""
|
||||
cur.execute(query, (f"uploads/{filename}", f"uploads/{filename}"))
|
||||
results = cur.fetchall()
|
||||
|
||||
# Check if user owns any of these warranties or is admin
|
||||
user_id = request.user['id']
|
||||
is_admin = request.user.get('is_admin', False)
|
||||
|
||||
authorized = is_admin # Admins can access all files
|
||||
|
||||
if not authorized and results:
|
||||
for warranty_id, warranty_user_id in results:
|
||||
if warranty_user_id == user_id:
|
||||
authorized = True
|
||||
break
|
||||
|
||||
if not authorized:
|
||||
logger.warning(f"Unauthorized file access attempt: {filename} by user {user_id}")
|
||||
return jsonify({"message": "You are not authorized to access this file"}), 403
|
||||
|
||||
# Serve the file securely
|
||||
return send_from_directory('/data/uploads', filename)
|
||||
finally:
|
||||
release_db_connection(conn)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in secure file access for {filename}: {e}")
|
||||
return jsonify({"message": "Error accessing file"}), 500
|
||||
|
||||
# Initialize the database when the application starts
|
||||
try:
|
||||
init_db()
|
||||
|
||||
126
frontend/file-utils.js
Normal file
126
frontend/file-utils.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Utility functions for secure file handling
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts a file path to a secure API endpoint path
|
||||
* @param {string} path - The file path to convert
|
||||
* @returns {string} The secure API endpoint path
|
||||
*/
|
||||
function secureFilePath(path) {
|
||||
if (!path) return '';
|
||||
|
||||
if (path.startsWith('uploads/')) {
|
||||
return '/api/secure-file/' + path.substring(8);
|
||||
}
|
||||
|
||||
if (path.startsWith('/uploads/')) {
|
||||
return '/api/secure-file/' + path.substring(9);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a file in a new tab with proper authentication
|
||||
* @param {string} path - The file path to open
|
||||
*/
|
||||
function openSecureFile(path) {
|
||||
if (!path) return;
|
||||
|
||||
const securePath = secureFilePath(path);
|
||||
console.log('Opening file:', securePath);
|
||||
|
||||
// Try to get the token from different sources
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
||||
if (!token) {
|
||||
console.error('No authentication token available');
|
||||
alert('You must be logged in to access files');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a link with target="_blank" and click it programmatically
|
||||
const link = document.createElement('a');
|
||||
link.href = securePath;
|
||||
link.target = '_blank';
|
||||
|
||||
// Add a click event listener to inject the token
|
||||
link.addEventListener('click', function(e) {
|
||||
// Prevent the default navigation
|
||||
e.preventDefault();
|
||||
|
||||
// Make a fetch request with the Authorization header
|
||||
fetch(securePath, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
// Create a URL for the blob and open it in a new window
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error opening file:', error);
|
||||
alert('Error opening file. Please try again or check if you are logged in.');
|
||||
});
|
||||
});
|
||||
|
||||
// Trigger the click event
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a file using the secure file endpoint
|
||||
* @param {string} path - The file path to download
|
||||
* @param {string} filename - The name to save the file as
|
||||
*/
|
||||
function downloadSecureFile(path, filename) {
|
||||
if (!path) return;
|
||||
|
||||
const securePath = secureFilePath(path);
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
||||
if (!token) {
|
||||
console.error('No authentication token available');
|
||||
alert('You must be logged in to download files');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(securePath, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = filename || path.split('/').pop();
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error downloading file:', error);
|
||||
alert('Error downloading file. Please try again or check if you are logged in.');
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,9 @@
|
||||
<!-- Include authentication script first to handle login state immediately -->
|
||||
<script src="include-auth-new.js"></script>
|
||||
|
||||
<!-- File utilities script for secure file handling -->
|
||||
<script src="file-utils.js"></script>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Warranty Tracker</title>
|
||||
|
||||
@@ -565,15 +565,13 @@ async function renderWarranties(warrantiesToRender) {
|
||||
</a>
|
||||
` : ''}
|
||||
${warranty.invoice_path ? `
|
||||
<a href="${warranty.invoice_path}" class="invoice-link" target="_blank">
|
||||
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;" class="invoice-link">
|
||||
<i class="fas fa-file-invoice"></i> Invoice
|
||||
</a>
|
||||
` : ''}
|
||||
</a>` : ''}
|
||||
${warranty.manual_path ? `
|
||||
<a href="${warranty.manual_path}" class="manual-link" target="_blank">
|
||||
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;" class="manual-link">
|
||||
<i class="fas fa-book"></i> Manual
|
||||
</a>
|
||||
` : ''}
|
||||
</a>` : ''}
|
||||
</div>
|
||||
`;
|
||||
} else if (currentView === 'list') {
|
||||
@@ -616,15 +614,13 @@ async function renderWarranties(warrantiesToRender) {
|
||||
</a>
|
||||
` : ''}
|
||||
${warranty.invoice_path ? `
|
||||
<a href="${warranty.invoice_path}" class="invoice-link" target="_blank">
|
||||
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;" class="invoice-link">
|
||||
<i class="fas fa-file-invoice"></i> Invoice
|
||||
</a>
|
||||
` : ''}
|
||||
</a>` : ''}
|
||||
${warranty.manual_path ? `
|
||||
<a href="${warranty.manual_path}" class="manual-link" target="_blank">
|
||||
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;" class="manual-link">
|
||||
<i class="fas fa-book"></i> Manual
|
||||
</a>
|
||||
` : ''}
|
||||
</a>` : ''}
|
||||
</div>
|
||||
`;
|
||||
} else if (currentView === 'table') {
|
||||
@@ -657,15 +653,13 @@ async function renderWarranties(warrantiesToRender) {
|
||||
</a>
|
||||
` : ''}
|
||||
${warranty.invoice_path ? `
|
||||
<a href="${warranty.invoice_path}" class="invoice-link" target="_blank">
|
||||
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;" class="invoice-link">
|
||||
<i class="fas fa-file-invoice"></i>
|
||||
</a>
|
||||
` : ''}
|
||||
</a>` : ''}
|
||||
${warranty.manual_path ? `
|
||||
<a href="${warranty.manual_path}" class="manual-link" target="_blank">
|
||||
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;" class="manual-link">
|
||||
<i class="fas fa-book"></i>
|
||||
</a>
|
||||
` : ''}
|
||||
</a>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -774,7 +768,7 @@ function openEditModal(warranty) {
|
||||
currentInvoiceElement.innerHTML = `
|
||||
<span class="text-success">
|
||||
<i class="fas fa-check-circle"></i> Current invoice:
|
||||
<a href="${warranty.invoice_path}" target="_blank">View</a>
|
||||
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;">View</a>
|
||||
(Upload a new file to replace)
|
||||
</span>
|
||||
`;
|
||||
@@ -790,7 +784,7 @@ function openEditModal(warranty) {
|
||||
currentManualElement.innerHTML = `
|
||||
<span class="text-success">
|
||||
<i class="fas fa-check-circle"></i> Current manual:
|
||||
<a href="${warranty.manual_path}" target="_blank">View</a>
|
||||
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;">View</a>
|
||||
(Upload a new file to replace)
|
||||
</span>
|
||||
`;
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
<!-- Include authentication script first to handle login state immediately -->
|
||||
<script src="include-auth-new.js"></script>
|
||||
|
||||
<!-- File utilities script for secure file handling -->
|
||||
<script src="file-utils.js"></script>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Status - Warranty Tracker</title>
|
||||
|
||||
@@ -613,7 +613,7 @@ function filterAndSortWarranties() {
|
||||
<a href="index.html?edit=${warranty.id}" class="action-btn edit-btn" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a href="${warranty.invoice_path}" target="_blank" class="action-btn view-btn" title="View Invoice" ${!warranty.invoice_path ? 'style="display: none;"' : ''}>
|
||||
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;" class="action-btn view-btn" title="View Invoice" ${!warranty.invoice_path ? 'style="display: none;"' : ''}>
|
||||
<i class="fas fa-file-alt"></i>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -62,13 +62,7 @@ server {
|
||||
|
||||
# Uploads - serve files from uploads directory
|
||||
location /uploads/ {
|
||||
alias /data/uploads/;
|
||||
autoindex on;
|
||||
|
||||
# No caching
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
return 403 "Access forbidden";
|
||||
}
|
||||
|
||||
# HTML files - ensure proper content type
|
||||
|
||||
Reference in New Issue
Block a user