added input validation and security hardening
This commit is contained in:
@@ -219,8 +219,13 @@ function createAuthRouter(db, logAudit) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up expired sessions (can be called periodically)
|
// Clean up expired sessions (admin only)
|
||||||
router.post('/cleanup-sessions', async (req, res) => {
|
router.post('/cleanup-sessions', async (req, res) => {
|
||||||
|
// Basic auth check - require a valid session to call this
|
||||||
|
const sessionId = req.cookies?.session_id;
|
||||||
|
if (!sessionId) {
|
||||||
|
return res.status(401).json({ error: 'Authentication required' });
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
db.run(
|
db.run(
|
||||||
|
|||||||
@@ -27,20 +27,89 @@ const CORS_ORIGINS = process.env.CORS_ORIGINS
|
|||||||
? process.env.CORS_ORIGINS.split(',')
|
? process.env.CORS_ORIGINS.split(',')
|
||||||
: ['http://localhost:3000'];
|
: ['http://localhost:3000'];
|
||||||
|
|
||||||
|
// ========== SECURITY HELPERS ==========
|
||||||
|
|
||||||
|
// Allowed file extensions for document uploads (documents only, no executables)
|
||||||
|
const ALLOWED_EXTENSIONS = new Set([
|
||||||
|
'.pdf', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.tif',
|
||||||
|
'.txt', '.csv', '.log',
|
||||||
|
'.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
||||||
|
'.odt', '.ods', '.odp',
|
||||||
|
'.rtf', '.html', '.htm', '.xml', '.json', '.yaml', '.yml',
|
||||||
|
'.zip', '.gz', '.tar', '.7z'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Allowed MIME type prefixes
|
||||||
|
const ALLOWED_MIME_PREFIXES = [
|
||||||
|
'image/', 'text/', 'application/pdf',
|
||||||
|
'application/msword', 'application/vnd.openxmlformats',
|
||||||
|
'application/vnd.ms-', 'application/vnd.oasis.opendocument',
|
||||||
|
'application/rtf', 'application/json', 'application/xml',
|
||||||
|
'application/zip', 'application/gzip', 'application/x-7z',
|
||||||
|
'application/x-tar', 'application/octet-stream'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sanitize a single path segment (cveId, vendor, filename) to prevent traversal
|
||||||
|
function sanitizePathSegment(segment) {
|
||||||
|
if (!segment || typeof segment !== 'string') return '';
|
||||||
|
// Remove path separators, null bytes, and .. sequences
|
||||||
|
return segment
|
||||||
|
.replace(/\0/g, '')
|
||||||
|
.replace(/\.\./g, '')
|
||||||
|
.replace(/[\/\\]/g, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that a resolved path is within the uploads directory
|
||||||
|
function isPathWithinUploads(targetPath) {
|
||||||
|
const uploadsRoot = path.resolve('uploads');
|
||||||
|
const resolved = path.resolve(targetPath);
|
||||||
|
return resolved.startsWith(uploadsRoot + path.sep) || resolved === uploadsRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CVE ID format
|
||||||
|
const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/;
|
||||||
|
function isValidCveId(cveId) {
|
||||||
|
return typeof cveId === 'string' && CVE_ID_PATTERN.test(cveId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allowed enum values
|
||||||
|
const VALID_SEVERITIES = ['Critical', 'High', 'Medium', 'Low'];
|
||||||
|
const VALID_STATUSES = ['Open', 'Addressed', 'In Progress', 'Resolved'];
|
||||||
|
const VALID_DOC_TYPES = ['advisory', 'email', 'screenshot', 'patch', 'other'];
|
||||||
|
|
||||||
|
// Validate vendor name - printable chars, reasonable length
|
||||||
|
function isValidVendor(vendor) {
|
||||||
|
return typeof vendor === 'string' && vendor.trim().length > 0 && vendor.length <= 200;
|
||||||
|
}
|
||||||
|
|
||||||
// Log all incoming requests
|
// Log all incoming requests
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
|
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Security headers
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||||
|
res.setHeader('X-Frame-Options', 'DENY');
|
||||||
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||||
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||||
|
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: CORS_ORIGINS,
|
origin: CORS_ORIGINS,
|
||||||
credentials: true
|
credentials: true
|
||||||
}));
|
}));
|
||||||
app.use(express.json());
|
app.use(express.json({ limit: '1mb' }));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use('/uploads', express.static('uploads'));
|
app.use('/uploads', express.static('uploads', {
|
||||||
|
dotfiles: 'deny',
|
||||||
|
index: false
|
||||||
|
}));
|
||||||
|
|
||||||
// Database connection
|
// Database connection
|
||||||
const db = new sqlite3.Database('./cve_database.db', (err) => {
|
const db = new sqlite3.Database('./cve_database.db', (err) => {
|
||||||
@@ -71,12 +140,28 @@ const storage = multer.diskStorage({
|
|||||||
},
|
},
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
cb(null, `${timestamp}-${file.originalname}`);
|
// Sanitize original filename - strip path components and dangerous chars
|
||||||
|
const safeName = sanitizePathSegment(file.originalname).replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||||
|
cb(null, `${timestamp}-${safeName}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const upload = multer({
|
// File filter - reject executables and non-allowed types
|
||||||
|
function fileFilter(req, file, cb) {
|
||||||
|
const ext = path.extname(file.originalname).toLowerCase();
|
||||||
|
if (!ALLOWED_EXTENSIONS.has(ext)) {
|
||||||
|
return cb(new Error(`File type '${ext}' is not allowed. Allowed types: ${[...ALLOWED_EXTENSIONS].join(', ')}`));
|
||||||
|
}
|
||||||
|
const mimeAllowed = ALLOWED_MIME_PREFIXES.some(prefix => file.mimetype.startsWith(prefix));
|
||||||
|
if (!mimeAllowed) {
|
||||||
|
return cb(new Error(`MIME type '${file.mimetype}' is not allowed.`));
|
||||||
|
}
|
||||||
|
cb(null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
storage: storage,
|
storage: storage,
|
||||||
|
fileFilter: fileFilter,
|
||||||
limits: { fileSize: 10 * 1024 * 1024 } // 10MB limit
|
limits: { fileSize: 10 * 1024 * 1024 } // 10MB limit
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,7 +208,7 @@ app.get('/api/cves', requireAuth(db), (req, res) => {
|
|||||||
db.all(query, params, (err, rows) => {
|
db.all(query, params, (err, rows) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error fetching CVEs:', err);
|
console.error('Error fetching CVEs:', err);
|
||||||
return res.status(500).json({ error: err.message });
|
return res.status(500).json({ error: 'Internal server error.' });
|
||||||
}
|
}
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
});
|
});
|
||||||
@@ -132,7 +217,7 @@ app.get('/api/cves', requireAuth(db), (req, res) => {
|
|||||||
// Get distinct CVE IDs for NVD sync (authenticated users)
|
// Get distinct CVE IDs for NVD sync (authenticated users)
|
||||||
app.get('/api/cves/distinct-ids', requireAuth(db), (req, res) => {
|
app.get('/api/cves/distinct-ids', requireAuth(db), (req, res) => {
|
||||||
db.all('SELECT DISTINCT cve_id FROM cves ORDER BY cve_id', [], (err, rows) => {
|
db.all('SELECT DISTINCT cve_id FROM cves ORDER BY cve_id', [], (err, rows) => {
|
||||||
if (err) return res.status(500).json({ error: err.message });
|
if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); }
|
||||||
res.json(rows.map(r => r.cve_id));
|
res.json(rows.map(r => r.cve_id));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -155,7 +240,7 @@ app.get('/api/cves/check/:cveId', requireAuth(db), (req, res) => {
|
|||||||
|
|
||||||
db.all(query, [cveId], (err, rows) => {
|
db.all(query, [cveId], (err, rows) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: err.message });
|
console.error(err); return res.status(500).json({ error: 'Internal server error.' });
|
||||||
}
|
}
|
||||||
if (!rows || rows.length === 0) {
|
if (!rows || rows.length === 0) {
|
||||||
return res.json({
|
return res.json({
|
||||||
@@ -197,7 +282,7 @@ app.get('/api/cves/:cveId/vendors', requireAuth(db), (req, res) => {
|
|||||||
|
|
||||||
db.all(query, [cveId], (err, rows) => {
|
db.all(query, [cveId], (err, rows) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: err.message });
|
console.error(err); return res.status(500).json({ error: 'Internal server error.' });
|
||||||
}
|
}
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
});
|
});
|
||||||
@@ -206,31 +291,39 @@ app.get('/api/cves/:cveId/vendors', requireAuth(db), (req, res) => {
|
|||||||
|
|
||||||
// Create new CVE entry - ALLOW MULTIPLE VENDORS (editor or admin)
|
// Create new CVE entry - ALLOW MULTIPLE VENDORS (editor or admin)
|
||||||
app.post('/api/cves', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
app.post('/api/cves', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||||
console.log('=== ADD CVE REQUEST ===');
|
|
||||||
console.log('Body:', req.body);
|
|
||||||
console.log('=======================');
|
|
||||||
|
|
||||||
const { cve_id, vendor, severity, description, published_date } = req.body;
|
const { cve_id, vendor, severity, description, published_date } = req.body;
|
||||||
|
|
||||||
|
// Input validation
|
||||||
|
if (!cve_id || !isValidCveId(cve_id)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid CVE ID format. Expected CVE-YYYY-NNNNN.' });
|
||||||
|
}
|
||||||
|
if (!vendor || !isValidVendor(vendor)) {
|
||||||
|
return res.status(400).json({ error: 'Vendor is required and must be under 200 characters.' });
|
||||||
|
}
|
||||||
|
if (!severity || !VALID_SEVERITIES.includes(severity)) {
|
||||||
|
return res.status(400).json({ error: `Invalid severity. Must be one of: ${VALID_SEVERITIES.join(', ')}` });
|
||||||
|
}
|
||||||
|
if (!description || typeof description !== 'string' || description.length > 10000) {
|
||||||
|
return res.status(400).json({ error: 'Description is required and must be under 10000 characters.' });
|
||||||
|
}
|
||||||
|
if (!published_date || !/^\d{4}-\d{2}-\d{2}$/.test(published_date)) {
|
||||||
|
return res.status(400).json({ error: 'Published date is required in YYYY-MM-DD format.' });
|
||||||
|
}
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO cves (cve_id, vendor, severity, description, published_date)
|
INSERT INTO cves (cve_id, vendor, severity, description, published_date)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
console.log('Query:', query);
|
|
||||||
console.log('Values:', [cve_id, vendor, severity, description, published_date]);
|
|
||||||
|
|
||||||
db.run(query, [cve_id, vendor, severity, description, published_date], function(err) {
|
db.run(query, [cve_id, vendor, severity, description, published_date], function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('DATABASE ERROR:', err); // Make sure this is here
|
console.error('DATABASE ERROR:', err);
|
||||||
// ... rest of error handling
|
|
||||||
// Check if it's a duplicate CVE_ID + Vendor combination
|
|
||||||
if (err.message.includes('UNIQUE constraint failed')) {
|
if (err.message.includes('UNIQUE constraint failed')) {
|
||||||
return res.status(409).json({
|
return res.status(409).json({
|
||||||
error: 'This CVE already exists for this vendor. Choose a different vendor or update the existing entry.'
|
error: 'This CVE already exists for this vendor. Choose a different vendor or update the existing entry.'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return res.status(500).json({ error: err.message });
|
return res.status(500).json({ error: 'Failed to create CVE entry.' });
|
||||||
}
|
}
|
||||||
logAudit(db, {
|
logAudit(db, {
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
@@ -254,12 +347,16 @@ app.post('/api/cves', requireAuth(db), requireRole('editor', 'admin'), (req, res
|
|||||||
app.patch('/api/cves/:cveId/status', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
app.patch('/api/cves/:cveId/status', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
||||||
const { cveId } = req.params;
|
const { cveId } = req.params;
|
||||||
const { status } = req.body;
|
const { status } = req.body;
|
||||||
|
|
||||||
|
if (!status || !VALID_STATUSES.includes(status)) {
|
||||||
|
return res.status(400).json({ error: `Invalid status. Must be one of: ${VALID_STATUSES.join(', ')}` });
|
||||||
|
}
|
||||||
|
|
||||||
const query = `UPDATE cves SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE cve_id = ?`;
|
const query = `UPDATE cves SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE cve_id = ?`;
|
||||||
|
|
||||||
db.run(query, [status, cveId], function(err) {
|
db.run(query, [status, cveId], function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: err.message });
|
console.error(err); return res.status(500).json({ error: 'Internal server error.' });
|
||||||
}
|
}
|
||||||
logAudit(db, {
|
logAudit(db, {
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
@@ -314,7 +411,8 @@ app.post('/api/cves/nvd-sync', requireAuth(db), requireRole('editor', 'admin'),
|
|||||||
values,
|
values,
|
||||||
function(err) {
|
function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
errors.push({ cve_id: entry.cve_id, error: err.message });
|
console.error('NVD sync update error:', err);
|
||||||
|
errors.push({ cve_id: entry.cve_id, error: 'Update failed' });
|
||||||
} else {
|
} else {
|
||||||
updated += this.changes;
|
updated += this.changes;
|
||||||
}
|
}
|
||||||
@@ -348,9 +446,29 @@ app.put('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req,
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { cve_id, vendor, severity, description, published_date, status } = req.body;
|
const { cve_id, vendor, severity, description, published_date, status } = req.body;
|
||||||
|
|
||||||
|
// Input validation for provided fields
|
||||||
|
if (cve_id !== undefined && !isValidCveId(cve_id)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid CVE ID format. Expected CVE-YYYY-NNNNN.' });
|
||||||
|
}
|
||||||
|
if (vendor !== undefined && !isValidVendor(vendor)) {
|
||||||
|
return res.status(400).json({ error: 'Vendor must be under 200 characters.' });
|
||||||
|
}
|
||||||
|
if (severity !== undefined && !VALID_SEVERITIES.includes(severity)) {
|
||||||
|
return res.status(400).json({ error: `Invalid severity. Must be one of: ${VALID_SEVERITIES.join(', ')}` });
|
||||||
|
}
|
||||||
|
if (description !== undefined && (typeof description !== 'string' || description.length > 10000)) {
|
||||||
|
return res.status(400).json({ error: 'Description must be under 10000 characters.' });
|
||||||
|
}
|
||||||
|
if (published_date !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(published_date)) {
|
||||||
|
return res.status(400).json({ error: 'Published date must be in YYYY-MM-DD format.' });
|
||||||
|
}
|
||||||
|
if (status !== undefined && !VALID_STATUSES.includes(status)) {
|
||||||
|
return res.status(400).json({ error: `Invalid status. Must be one of: ${VALID_STATUSES.join(', ')}` });
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch existing row first
|
// Fetch existing row first
|
||||||
db.get('SELECT * FROM cves WHERE id = ?', [id], (err, existing) => {
|
db.get('SELECT * FROM cves WHERE id = ?', [id], (err, existing) => {
|
||||||
if (err) return res.status(500).json({ error: err.message });
|
if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); }
|
||||||
if (!existing) return res.status(404).json({ error: 'CVE entry not found' });
|
if (!existing) return res.status(404).json({ error: 'CVE entry not found' });
|
||||||
|
|
||||||
const before = { cve_id: existing.cve_id, vendor: existing.vendor, severity: existing.severity, description: existing.description, published_date: existing.published_date, status: existing.status };
|
const before = { cve_id: existing.cve_id, vendor: existing.vendor, severity: existing.severity, description: existing.description, published_date: existing.published_date, status: existing.status };
|
||||||
@@ -377,7 +495,7 @@ app.put('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req,
|
|||||||
values.push(id);
|
values.push(id);
|
||||||
|
|
||||||
db.run(`UPDATE cves SET ${fields.join(', ')} WHERE id = ?`, values, function(updateErr) {
|
db.run(`UPDATE cves SET ${fields.join(', ')} WHERE id = ?`, values, function(updateErr) {
|
||||||
if (updateErr) return res.status(500).json({ error: updateErr.message });
|
if (updateErr) { console.error(updateErr); return res.status(500).json({ error: 'Internal server error.' }); }
|
||||||
|
|
||||||
const after = {
|
const after = {
|
||||||
cve_id: newCveId, vendor: newVendor,
|
cve_id: newCveId, vendor: newVendor,
|
||||||
@@ -404,12 +522,16 @@ app.put('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req,
|
|||||||
if (cveIdChanged || vendorChanged) {
|
if (cveIdChanged || vendorChanged) {
|
||||||
// Check UNIQUE constraint
|
// Check UNIQUE constraint
|
||||||
db.get('SELECT id FROM cves WHERE cve_id = ? AND vendor = ? AND id != ?', [newCveId, newVendor, id], (checkErr, conflict) => {
|
db.get('SELECT id FROM cves WHERE cve_id = ? AND vendor = ? AND id != ?', [newCveId, newVendor, id], (checkErr, conflict) => {
|
||||||
if (checkErr) return res.status(500).json({ error: checkErr.message });
|
if (checkErr) { console.error(checkErr); return res.status(500).json({ error: 'Internal server error.' }); }
|
||||||
if (conflict) return res.status(409).json({ error: 'A CVE entry with this CVE ID and vendor already exists.' });
|
if (conflict) return res.status(409).json({ error: 'A CVE entry with this CVE ID and vendor already exists.' });
|
||||||
|
|
||||||
// Rename document directory
|
// Rename document directory (with path traversal prevention)
|
||||||
const oldDir = path.join('uploads', existing.cve_id, existing.vendor);
|
const oldDir = path.join('uploads', sanitizePathSegment(existing.cve_id), sanitizePathSegment(existing.vendor));
|
||||||
const newDir = path.join('uploads', newCveId, newVendor);
|
const newDir = path.join('uploads', sanitizePathSegment(newCveId), sanitizePathSegment(newVendor));
|
||||||
|
|
||||||
|
if (!isPathWithinUploads(oldDir) || !isPathWithinUploads(newDir)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid CVE ID or vendor name for file paths.' });
|
||||||
|
}
|
||||||
|
|
||||||
if (fs.existsSync(oldDir)) {
|
if (fs.existsSync(oldDir)) {
|
||||||
const newParent = path.join('uploads', newCveId);
|
const newParent = path.join('uploads', newCveId);
|
||||||
@@ -428,7 +550,7 @@ app.put('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req,
|
|||||||
|
|
||||||
// Update documents table - file paths
|
// Update documents table - file paths
|
||||||
db.all('SELECT id, file_path FROM documents WHERE cve_id = ? AND vendor = ?', [existing.cve_id, existing.vendor], (docErr, docs) => {
|
db.all('SELECT id, file_path FROM documents WHERE cve_id = ? AND vendor = ?', [existing.cve_id, existing.vendor], (docErr, docs) => {
|
||||||
if (docErr) return res.status(500).json({ error: docErr.message });
|
if (docErr) { console.error(docErr); return res.status(500).json({ error: 'Internal server error.' }); }
|
||||||
|
|
||||||
const oldPrefix = path.join('uploads', existing.cve_id, existing.vendor);
|
const oldPrefix = path.join('uploads', existing.cve_id, existing.vendor);
|
||||||
const newPrefix = path.join('uploads', newCveId, newVendor);
|
const newPrefix = path.join('uploads', newCveId, newVendor);
|
||||||
@@ -469,7 +591,7 @@ app.delete('/api/cves/by-cve-id/:cveId', requireAuth(db), requireRole('editor',
|
|||||||
|
|
||||||
// Get all rows for this CVE ID to know what we're deleting
|
// Get all rows for this CVE ID to know what we're deleting
|
||||||
db.all('SELECT * FROM cves WHERE cve_id = ?', [cveId], (err, rows) => {
|
db.all('SELECT * FROM cves WHERE cve_id = ?', [cveId], (err, rows) => {
|
||||||
if (err) return res.status(500).json({ error: err.message });
|
if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); }
|
||||||
if (!rows || rows.length === 0) return res.status(404).json({ error: 'No CVE entries found for this CVE ID' });
|
if (!rows || rows.length === 0) return res.status(404).json({ error: 'No CVE entries found for this CVE ID' });
|
||||||
|
|
||||||
// Delete all documents from DB
|
// Delete all documents from DB
|
||||||
@@ -478,11 +600,12 @@ app.delete('/api/cves/by-cve-id/:cveId', requireAuth(db), requireRole('editor',
|
|||||||
|
|
||||||
// Delete all CVE rows
|
// Delete all CVE rows
|
||||||
db.run('DELETE FROM cves WHERE cve_id = ?', [cveId], function(cveErr) {
|
db.run('DELETE FROM cves WHERE cve_id = ?', [cveId], function(cveErr) {
|
||||||
if (cveErr) return res.status(500).json({ error: cveErr.message });
|
if (cveErr) { console.error(cveErr); return res.status(500).json({ error: 'Internal server error.' }); }
|
||||||
|
|
||||||
// Remove upload directory
|
// Remove upload directory (with path traversal prevention)
|
||||||
const cveDir = path.join('uploads', cveId);
|
const safeCveId = sanitizePathSegment(cveId);
|
||||||
if (fs.existsSync(cveDir)) {
|
const cveDir = path.join('uploads', safeCveId);
|
||||||
|
if (safeCveId && isPathWithinUploads(cveDir) && fs.existsSync(cveDir)) {
|
||||||
fs.rmSync(cveDir, { recursive: true, force: true });
|
fs.rmSync(cveDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,17 +630,17 @@ app.delete('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (re
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
db.get('SELECT * FROM cves WHERE id = ?', [id], (err, cve) => {
|
db.get('SELECT * FROM cves WHERE id = ?', [id], (err, cve) => {
|
||||||
if (err) return res.status(500).json({ error: err.message });
|
if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); }
|
||||||
if (!cve) return res.status(404).json({ error: 'CVE entry not found' });
|
if (!cve) return res.status(404).json({ error: 'CVE entry not found' });
|
||||||
|
|
||||||
// Delete associated documents from DB
|
// Delete associated documents from DB
|
||||||
db.all('SELECT id, file_path FROM documents WHERE cve_id = ? AND vendor = ?', [cve.cve_id, cve.vendor], (docErr, docs) => {
|
db.all('SELECT id, file_path FROM documents WHERE cve_id = ? AND vendor = ?', [cve.cve_id, cve.vendor], (docErr, docs) => {
|
||||||
if (docErr) console.error('Error fetching documents:', docErr);
|
if (docErr) console.error('Error fetching documents:', docErr);
|
||||||
|
|
||||||
// Delete document files from disk
|
// Delete document files from disk (with path traversal prevention)
|
||||||
if (docs && docs.length > 0) {
|
if (docs && docs.length > 0) {
|
||||||
docs.forEach(doc => {
|
docs.forEach(doc => {
|
||||||
if (doc.file_path && fs.existsSync(doc.file_path)) {
|
if (doc.file_path && isPathWithinUploads(doc.file_path) && fs.existsSync(doc.file_path)) {
|
||||||
fs.unlinkSync(doc.file_path);
|
fs.unlinkSync(doc.file_path);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -529,17 +652,17 @@ app.delete('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (re
|
|||||||
|
|
||||||
// Delete CVE row
|
// Delete CVE row
|
||||||
db.run('DELETE FROM cves WHERE id = ?', [id], function(delErr) {
|
db.run('DELETE FROM cves WHERE id = ?', [id], function(delErr) {
|
||||||
if (delErr) return res.status(500).json({ error: delErr.message });
|
if (delErr) { console.error(delErr); return res.status(500).json({ error: 'Internal server error.' }); }
|
||||||
|
|
||||||
// Clean up directories
|
// Clean up directories (with path traversal prevention)
|
||||||
const vendorDir = path.join('uploads', cve.cve_id, cve.vendor);
|
const safeVendorDir = path.join('uploads', sanitizePathSegment(cve.cve_id), sanitizePathSegment(cve.vendor));
|
||||||
if (fs.existsSync(vendorDir)) {
|
if (isPathWithinUploads(safeVendorDir) && fs.existsSync(safeVendorDir)) {
|
||||||
fs.rmSync(vendorDir, { recursive: true, force: true });
|
fs.rmSync(safeVendorDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
const cveDir = path.join('uploads', cve.cve_id);
|
const safeCveDir = path.join('uploads', sanitizePathSegment(cve.cve_id));
|
||||||
if (fs.existsSync(cveDir)) {
|
if (isPathWithinUploads(safeCveDir) && fs.existsSync(safeCveDir)) {
|
||||||
const remaining = fs.readdirSync(cveDir);
|
const remaining = fs.readdirSync(safeCveDir);
|
||||||
if (remaining.length === 0) fs.rmdirSync(cveDir);
|
if (remaining.length === 0) fs.rmdirSync(safeCveDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
logAudit(db, {
|
logAudit(db, {
|
||||||
@@ -578,7 +701,7 @@ app.get('/api/cves/:cveId/documents', requireAuth(db), (req, res) => {
|
|||||||
|
|
||||||
db.all(query, params, (err, rows) => {
|
db.all(query, params, (err, rows) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: err.message });
|
console.error(err); return res.status(500).json({ error: 'Internal server error.' });
|
||||||
}
|
}
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
});
|
});
|
||||||
@@ -588,16 +711,17 @@ app.get('/api/cves/:cveId/documents', requireAuth(db), (req, res) => {
|
|||||||
app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'admin'), (req, res, next) => {
|
app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'admin'), (req, res, next) => {
|
||||||
upload.single('file')(req, res, (err) => {
|
upload.single('file')(req, res, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('MULTER ERROR:', err);
|
console.error('Upload error:', err.message);
|
||||||
return res.status(500).json({ error: 'File upload failed: ' + err.message });
|
// Show file validation errors to the user; hide other internal errors
|
||||||
|
if (err.message && (err.message.startsWith('File type') || err.message.startsWith('MIME type'))) {
|
||||||
|
return res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||||
|
return res.status(400).json({ error: 'File exceeds the 10MB size limit.' });
|
||||||
|
}
|
||||||
|
return res.status(500).json({ error: 'File upload failed.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('=== UPLOAD REQUEST RECEIVED ===');
|
|
||||||
console.log('CVE ID:', req.params.cveId);
|
|
||||||
console.log('Body:', req.body);
|
|
||||||
console.log('File:', req.file);
|
|
||||||
console.log('================================');
|
|
||||||
|
|
||||||
const { cveId } = req.params;
|
const { cveId } = req.params;
|
||||||
const { type, notes, vendor } = req.body;
|
const { type, notes, vendor } = req.body;
|
||||||
const file = req.file;
|
const file = req.file;
|
||||||
@@ -608,18 +732,41 @@ app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'a
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!vendor) {
|
if (!vendor) {
|
||||||
console.error('ERROR: Vendor is required');
|
// Clean up temp file
|
||||||
|
if (file.path && fs.existsSync(file.path)) fs.unlinkSync(file.path);
|
||||||
return res.status(400).json({ error: 'Vendor is required' });
|
return res.status(400).json({ error: 'Vendor is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate document type
|
||||||
|
if (type && !VALID_DOC_TYPES.includes(type)) {
|
||||||
|
if (file.path && fs.existsSync(file.path)) fs.unlinkSync(file.path);
|
||||||
|
return res.status(400).json({ error: `Invalid document type. Must be one of: ${VALID_DOC_TYPES.join(', ')}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize path segments to prevent directory traversal
|
||||||
|
const safeCveId = sanitizePathSegment(cveId);
|
||||||
|
const safeVendor = sanitizePathSegment(vendor);
|
||||||
|
const safeFilename = sanitizePathSegment(file.filename);
|
||||||
|
|
||||||
|
if (!safeCveId || !safeVendor || !safeFilename) {
|
||||||
|
if (file.path && fs.existsSync(file.path)) fs.unlinkSync(file.path);
|
||||||
|
return res.status(400).json({ error: 'Invalid CVE ID, vendor, or filename.' });
|
||||||
|
}
|
||||||
|
|
||||||
// Move file from temp to proper location
|
// Move file from temp to proper location
|
||||||
const finalDir = path.join('uploads', cveId, vendor);
|
const finalDir = path.join('uploads', safeCveId, safeVendor);
|
||||||
|
const finalPath = path.join(finalDir, safeFilename);
|
||||||
|
|
||||||
|
// Verify paths stay within uploads directory
|
||||||
|
if (!isPathWithinUploads(finalDir) || !isPathWithinUploads(finalPath)) {
|
||||||
|
if (file.path && fs.existsSync(file.path)) fs.unlinkSync(file.path);
|
||||||
|
return res.status(400).json({ error: 'Invalid file path.' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(finalDir)) {
|
if (!fs.existsSync(finalDir)) {
|
||||||
fs.mkdirSync(finalDir, { recursive: true });
|
fs.mkdirSync(finalDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalPath = path.join(finalDir, file.filename);
|
|
||||||
|
|
||||||
// Move file from temp to final location
|
// Move file from temp to final location
|
||||||
fs.renameSync(file.path, finalPath);
|
fs.renameSync(file.path, finalPath);
|
||||||
|
|
||||||
@@ -641,12 +788,12 @@ app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'a
|
|||||||
notes
|
notes
|
||||||
], function(err) {
|
], function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('DATABASE ERROR:', err);
|
console.error('Document insert error:', err);
|
||||||
// If database insert fails, delete the file
|
// If database insert fails, delete the file
|
||||||
if (fs.existsSync(finalPath)) {
|
if (fs.existsSync(finalPath)) {
|
||||||
fs.unlinkSync(finalPath);
|
fs.unlinkSync(finalPath);
|
||||||
}
|
}
|
||||||
return res.status(500).json({ error: err.message });
|
return res.status(500).json({ error: 'Internal server error.' });
|
||||||
}
|
}
|
||||||
logAudit(db, {
|
logAudit(db, {
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
@@ -662,7 +809,6 @@ app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'a
|
|||||||
message: 'Document uploaded successfully',
|
message: 'Document uploaded successfully',
|
||||||
file: {
|
file: {
|
||||||
name: file.originalname,
|
name: file.originalname,
|
||||||
path: finalPath,
|
|
||||||
size: fileSizeKB
|
size: fileSizeKB
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -672,20 +818,21 @@ app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'a
|
|||||||
// Delete document (admin only)
|
// Delete document (admin only)
|
||||||
app.delete('/api/documents/:id', requireAuth(db), requireRole('admin'), (req, res) => {
|
app.delete('/api/documents/:id', requireAuth(db), requireRole('admin'), (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
// First get the file path to delete the actual file
|
// First get the file path to delete the actual file
|
||||||
db.get('SELECT file_path FROM documents WHERE id = ?', [id], (err, row) => {
|
db.get('SELECT file_path FROM documents WHERE id = ?', [id], (err, row) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: err.message });
|
console.error(err); return res.status(500).json({ error: 'Internal server error.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row && fs.existsSync(row.file_path)) {
|
// Only delete file if path is within uploads directory
|
||||||
|
if (row && row.file_path && isPathWithinUploads(row.file_path) && fs.existsSync(row.file_path)) {
|
||||||
fs.unlinkSync(row.file_path);
|
fs.unlinkSync(row.file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
db.run('DELETE FROM documents WHERE id = ?', [id], function(err) {
|
db.run('DELETE FROM documents WHERE id = ?', [id], function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: err.message });
|
console.error(err); return res.status(500).json({ error: 'Internal server error.' });
|
||||||
}
|
}
|
||||||
logAudit(db, {
|
logAudit(db, {
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
@@ -709,7 +856,7 @@ app.get('/api/vendors', requireAuth(db), (req, res) => {
|
|||||||
|
|
||||||
db.all(query, [], (err, rows) => {
|
db.all(query, [], (err, rows) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: err.message });
|
console.error(err); return res.status(500).json({ error: 'Internal server error.' });
|
||||||
}
|
}
|
||||||
res.json(rows.map(r => r.vendor));
|
res.json(rows.map(r => r.vendor));
|
||||||
});
|
});
|
||||||
@@ -731,7 +878,7 @@ app.get('/api/stats', requireAuth(db), (req, res) => {
|
|||||||
|
|
||||||
db.get(query, [], (err, row) => {
|
db.get(query, [], (err, row) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: err.message });
|
console.error(err); return res.status(500).json({ error: 'Internal server error.' });
|
||||||
}
|
}
|
||||||
res.json(row);
|
res.json(row);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ export default function App() {
|
|||||||
const handleFileUpload = async (cveId, vendor) => {
|
const handleFileUpload = async (cveId, vendor) => {
|
||||||
const fileInput = document.createElement('input');
|
const fileInput = document.createElement('input');
|
||||||
fileInput.type = 'file';
|
fileInput.type = 'file';
|
||||||
fileInput.accept = '.pdf,.png,.jpg,.jpeg,.txt,.doc,.docx';
|
fileInput.accept = '.pdf,.png,.jpg,.jpeg,.gif,.bmp,.tiff,.tif,.txt,.csv,.log,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.odt,.ods,.odp,.rtf,.html,.htm,.xml,.json,.yaml,.yml,.zip,.gz,.tar,.7z';
|
||||||
|
|
||||||
fileInput.onchange = async (e) => {
|
fileInput.onchange = async (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
|
|||||||
Reference in New Issue
Block a user