fix(security): audit fixes #4 #6 #10 + hero rewrite (batch 0.5.2)

- #4: Replace su-exec with USER nodejs in Dockerfile (P0)
- #6: Add UNIQUE constraint on leads.email with migration (P1)
- #10: Consistent NULL handling for optional fields (P1)
- Hero section rewrite: B2B value proposition, prominent 8x8 badge
- Clean up .bak file left by agent
This commit is contained in:
null 2026-05-17 14:44:34 -05:00
parent 851759ae5e
commit 7d476f36e8
5 changed files with 148 additions and 46 deletions

View File

@ -40,9 +40,8 @@ ENV ZOHO_REDIRECT_URI=
# Create app directory structure
RUN mkdir -p /app/db /app/logs
# Copy entrypoint script
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Set permissions for db directory (before USER switch)
RUN chown -R nodejs:nodejs /app/db /app/logs
# Copy from builder - built artifacts and package manifests
COPY --from=builder /app/package.json /app/package-lock.json* ./
@ -52,17 +51,15 @@ COPY --from=builder /app/server ./server
# Install production dependencies only in runtime stage
RUN npm ci --omit=dev
# Install su-exec for switching to non-root user
RUN apk add --no-cache su-exec && \
rm -rf /var/cache/apk/*
# Expose backend port
EXPOSE 3001
# Switch to non-root user (standard approach, no su-exec needed)
USER nodejs
# Health check using Node 20 built-in fetch (no wget required)
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "fetch('http://localhost:3001/api/health').then(r => r.ok ? 0 : 1).catch(() => 1)" || exit 1
# Run the Express server via entrypoint (runs as root, then switches to nodejs)
ENTRYPOINT ["docker-entrypoint.sh"]
# Run the Express server
CMD ["node", "server/index.js"]

View File

@ -5,6 +5,10 @@
set -e
log_error() {
echo "[$(date -Iseconds)] ERROR $1" >&2
}
# Create directories if they don't exist
mkdir -p /app/db
mkdir -p /app/logs
@ -13,5 +17,12 @@ mkdir -p /app/logs
chmod 777 /app/db
chmod 777 /app/logs
# Issue #4: Check if nodejs user exists - if not, this is a Docker build error
if ! id nodejs >/dev/null 2>&1; then
log_error "nodejs user does not exist - this is a Docker build error"
exit 1
fi
# Run the Express server as nodejs user
# Issue #4: Exit with error code 1 if su-exec fails instead of falling back to root
exec su-exec nodejs node server/index.js

View File

@ -3,6 +3,17 @@ import path from 'path'
import { existsSync, mkdirSync, chmodSync } from 'fs'
import sqlite3 from 'better-sqlite3'
// --- Logger ---
const LOG_LEVELS = { error: 0, warn: 1, info: 2, debug: 3 }
const currentLevel = LOG_LEVELS[process.env.LOG_LEVEL?.toLowerCase()] ?? LOG_LEVELS.info
const log = {
info: (...args) => { if (currentLevel >= LOG_LEVELS.info) console.log(`[${new Date().toISOString()}] INFO `, ...args) },
warn: (...args) => { if (currentLevel >= LOG_LEVELS.warn) console.warn(`[${new Date().toISOString()}] WARN `, ...args) },
error: (...args) => { if (currentLevel >= LOG_LEVELS.error) console.error(`[${new Date().toISOString()}] ERROR`, ...args) },
debug: (...args) => { if (currentLevel >= LOG_LEVELS.debug) console.debug(`[${new Date().toISOString()}] DEBUG`, ...args) },
}
const dbPath = path.join(new URL(import.meta.url).pathname, '..', 'db', 'queuenorth.db')
const dbDir = path.dirname(dbPath)
@ -22,34 +33,86 @@ export const db = sqlite3(dbPath)
// Initialize schema
export const initSchema = () => {
// Leads table
db.exec(`
CREATE TABLE IF NOT EXISTS leads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
company TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
zip TEXT,
message TEXT,
service_interest TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
// Issue #6: Add UNIQUE constraint on leads.email with migration support
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='leads'").get()
if (tableExists) {
// Check if UNIQUE constraint already exists
const pragma = db.prepare("PRAGMA table_info(leads)").all()
const emailCol = pragma.find(col => col.name === 'email')
if (!emailCol || !emailCol.notnull) {
// UNIQUE constraint doesn't exist, need to add it via migration
log.info('[DB] Adding UNIQUE constraint on leads.email via migration')
// Migrate leads table to add UNIQUE constraint
db.exec(
[
'CREATE TABLE IF NOT EXISTS leads_new (',
' id INTEGER PRIMARY KEY AUTOINCREMENT,',
' company TEXT NOT NULL,',
' name TEXT NOT NULL,',
' email TEXT NOT NULL UNIQUE,',
' phone TEXT,',
' zip TEXT,',
' message TEXT,',
' service_interest TEXT,',
' created_at DATETIME DEFAULT CURRENT_TIMESTAMP',
')'
].join('\n')
)
// Copy existing data (deduplicate - keep first occurrence per email)
db.exec(
[
'INSERT OR IGNORE INTO leads_new (id, company, name, email, phone, zip, message, service_interest, created_at)',
'SELECT id, company, name, email, phone, zip, message, service_interest, created_at',
'FROM leads'
].join('\n')
)
// Drop old table
db.exec('DROP TABLE leads')
// Rename new table
db.exec('ALTER TABLE leads_new RENAME TO leads')
log.info('[DB] UNIQUE constraint added on leads.email')
}
} else {
// New table - create with UNIQUE constraint from the start
db.exec(
[
'CREATE TABLE IF NOT EXISTS leads (',
' id INTEGER PRIMARY KEY AUTOINCREMENT,',
' company TEXT NOT NULL,',
' name TEXT NOT NULL,',
' email TEXT NOT NULL UNIQUE,',
' phone TEXT,',
' zip TEXT,',
' message TEXT,',
' service_interest TEXT,',
' created_at DATETIME DEFAULT CURRENT_TIMESTAMP',
')'
].join('\n')
)
`)
}
// Support requests table
db.exec(`
CREATE TABLE IF NOT EXISTS support_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
company TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
issue TEXT NOT NULL,
priority TEXT DEFAULT 'medium',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
db.exec(
[
'CREATE TABLE IF NOT EXISTS support_requests (',
' id INTEGER PRIMARY KEY AUTOINCREMENT,',
' name TEXT NOT NULL,',
' company TEXT NOT NULL,',
' email TEXT NOT NULL,',
' phone TEXT,',
' issue TEXT NOT NULL,',
' priority TEXT DEFAULT \'medium\',',
' created_at DATETIME DEFAULT CURRENT_TIMESTAMP',
')'
].join('\n')
)
}
initSchema()

View File

@ -167,7 +167,9 @@ const sanitizeString = (input, maxLength) => {
sanitized = sanitized.replace(/<script[^>]*>.*?<\/script>/gi, '')
sanitized = sanitized.replace(/<[^>]*>/g, '')
// Truncate to max length
return sanitized.substring(0, maxLength)
sanitized = sanitized.substring(0, maxLength)
// Convert empty strings to undefined so they become NULL in DB
return sanitized === '' ? undefined : sanitized
}
const sanitizePayload = (data, fields) => {
@ -359,6 +361,24 @@ app.post('/api/leads', (req, res) => {
res.json({ success: true, message: "Thanks! We'll be in touch shortly." })
} catch (err) {
// Issue #6: Handle duplicate email error with 409 Conflict
const errorMsg = err.message?.toLowerCase() || ''
if (errorMsg.includes('unique constraint') || errorMsg.includes('duplicate')) {
log.warn(`Duplicate lead email: ${sanitized.email}`)
// Still forward to Zoho (non-blocking) for existing leads
try {
forwardToZoho(sanitized)
} catch (zohoErr) {
log.warn(`[Zoho] Skipped forwarding for duplicate lead: ${sanitized.email}`)
}
return res.status(409).json({
error: 'Duplicate lead',
message: 'A lead with this email already exists'
})
}
log.error('Error submitting lead:', err)
res.status(500).json({ error: 'Failed to submit lead' })
}

View File

@ -11,29 +11,40 @@ const Home = () => {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Hero Section */}
<section className="bg-primary-navy text-white py-16 md:py-24">
<section className="bg-gradient-to-br from-primary-navy via-primary-navy to-teal-900 text-white py-16 md:py-24">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6">
Modern Communications Infrastructure Without the Vendor Noise
Reliable Business Communications Without the Runaround
</h1>
<p className="text-xl md:text-2xl text-section-alt mb-8 max-w-2xl">
We deliver UCaaS, Contact Center, deployment, and managed lifecycle support for SMB and enterprise organizations.
We handle your phones, internet, and IT so you can focus on running your business. 8x8 Certified Partner with 25+ years of proven reliability.
</p>
<div className="flex flex-col sm:flex-row gap-4">
<Button variant="default" size="lg" className="bg-white text-primary-navy hover:bg-gray-100">
Request Consultation
<Button variant="default" size="lg" className="bg-white text-primary-navy hover:bg-gray-100" onClick={() => navigate('/contact')}>
Schedule Consultation
</Button>
<Button variant="outline" size="lg" className="border-white text-white hover:bg-white/10">
Explore Services
<Button variant="outline" size="lg" className="border-white text-white hover:bg-white/10" onClick={() => navigate('/services')}>
View Services
</Button>
</div>
<div className="mt-10 flex flex-wrap gap-4">
<span className="px-4 py-2 bg-white/10 rounded-full text-sm font-medium">8x8 Certified Partner</span>
<span className="px-4 py-2 bg-white/10 rounded-full text-sm font-medium">Veteran Owned</span>
<span className="px-4 py-2 bg-white/10 rounded-full text-sm font-numeric">25+ Years Experience</span>
<span className="px-4 py-2 bg-white/10 rounded-full text-sm font-medium">SMB to Enterprise</span>
<div className="flex items-center gap-2 px-4 py-2 bg-white/20 rounded-lg text-sm font-medium">
<img src="/assets/8x8_Logo_White.svg" alt="8x8" className="h-5 w-5" />
<span>8x8 Certified Partner</span>
</div>
<div className="flex items-center gap-2 px-4 py-2 bg-white/20 rounded-lg text-sm font-medium">
<div className="w-5 h-5 bg-white rounded-full flex items-center justify-center text-primary-navy font-bold">V</div>
<span>Veteran Owned</span>
</div>
<div className="flex items-center gap-2 px-4 py-2 bg-white/20 rounded-lg text-sm font-numeric">
<span>25+</span>
<span className="text-sm">Years Experience</span>
</div>
<div className="flex items-center gap-2 px-4 py-2 bg-white/20 rounded-lg text-sm font-medium">
<span>SMB to Enterprise</span>
</div>
</div>
</div>
<div className="hidden lg:block">