Files
open-isp/docs/IMPLEMENTATION_GUIDE_SECURITY.md
T
oxpoe eaf05f2811 OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor,
admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS.

Kredit & lisensi (ISC):
- Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak
  atas base kode dan repo yang bermanfaat.
- Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder.
- Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
2026-10-09 05:51:21 +07:00

18 KiB
Raw Blame History

IMPLEMENTATION GUIDE - SECURITY FIXES

Status: READY TO IMPLEMENT
Tanggal: 14 Agustus 2026
Target Completion: 2-3 minggu


🚀 QUICK START - CRITICAL FIXES

STEP 1: Install Required Packages

npm install bcrypt dotenv helmet express-rate-limit
npm install --save-dev @types/bcrypt

# Optional but recommended:
npm install express-validator joi
npm install better-sqlite3-session-store

STEP 2: Create .env File

# Copy template
cp .env.example .env

# Edit .env dengan credentials actual
# Edit dengan text editor
nano .env

File .env template:

# ======== SECURITY ========
NODE_ENV=development
SESSION_SECRET=GenerateRandomStringHere_$(openssl rand -hex 32)
ADMIN_PASSWORD_HASH=Will_Be_Set_By_Setup_Script

# ======== GENIEACS ========
GENIEACS_URL=http://192.168.8.189:7557
GENIEACS_USERNAME=admin
GENIEACS_PASSWORD=ChangeThisPassword

# ======== DATABASE ========
DATABASE_PATH=./database/billing.db

# ======== PAYMENT GATEWAYS ========
MIDTRANS_SERVER_KEY=Mid_server_xxxxx
MIDTRANS_MODE=sandbox

XENDIT_API_KEY=xnd_development_xxxxx

TRIPAY_API_KEY=xxxxx
TRIPAY_MERCHANT_CODE=xxxxx
TRIPAY_MODE=sandbox

DUITKU_API_KEY=xxxxx
DUITKU_MERCHANT_CODE=xxxxx

# ======== THIRD PARTY ========
DIGIFLAZZ_USERNAME=xxxxx
DIGIFLAZZ_API_KEY=xxxxx
DIGIFLAZZ_WEBHOOK_SECRET=xxxxx

# ======== WHATSAPP ========
WHATSAPP_ENABLED=true

# ======== TELEGRAM ========
TELEGRAM_ENABLED=false
TELEGRAM_BOT_TOKEN=xxxxx

# ======== RADIUS ========
RADIUS_SECRET=secret123
RADIUS_AUTH_PORT=1812
RADIUS_ACCT_PORT=1813

# ======== LOGGING ========
LOG_LEVEL=info

STEP 3: Update .gitignore

# Add these lines to .gitignore
.env
.env.local
.env.*.local
settings.json  # Optional: jika ada credentials di sini

🔐 FIX #1: Password Hashing with Bcrypt

Part A: Create Password Helper Module

File: utils/passwordHelper.js

const bcrypt = require('bcrypt');
const SALT_ROUNDS = 10;

/**
 * Hash password dengan bcrypt
 * @param {string} plainPassword - Plain text password
 * @returns {Promise<string>} Hashed password
 */
async function hashPassword(plainPassword) {
  if (!plainPassword) throw new Error('Password cannot be empty');
  return await bcrypt.hash(plainPassword, SALT_ROUNDS);
}

/**
 * Verify password
 * @param {string} plainPassword - Plain text password to verify
 * @param {string} hashedPassword - Hashed password from database
 * @returns {Promise<boolean>} True if password matches
 */
async function verifyPassword(plainPassword, hashedPassword) {
  if (!plainPassword || !hashedPassword) return false;
  return await bcrypt.compare(plainPassword, hashedPassword);
}

module.exports = {
  hashPassword,
  verifyPassword
};

Part B: Create Admin Setup Script

File: scripts/setup-admin-password.js

#!/usr/bin/env node
const readline = require('readline');
const db = require('../config/database');
const { hashPassword } = require('../utils/passwordHelper');
const { logger } = require('../config/logger');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

const question = (prompt) => new Promise((resolve) => {
  rl.question(prompt, resolve);
});

async function setupAdmin() {
  console.log('\n🔐 SETUP ADMIN PASSWORD\n');
  
  try {
    // Check if admin exists
    const admin = db.prepare('SELECT id FROM administrators LIMIT 1').get();
    
    if (!admin) {
      console.log('⚠️  No administrator found in database.');
      console.log('Please run database migration first.\n');
      rl.close();
      process.exit(1);
    }

    const username = await question('Admin Username [admin]: ');
    const password = await question('Admin Password (min 8 chars): ');
    const passwordConfirm = await question('Confirm Password: ');

    // Validation
    if (!username.trim() || !password) {
      console.log('❌ Username and password cannot be empty');
      rl.close();
      process.exit(1);
    }

    if (password !== passwordConfirm) {
      console.log('❌ Passwords do not match');
      rl.close();
      process.exit(1);
    }

    if (password.length < 8) {
      console.log('❌ Password must be at least 8 characters');
      rl.close();
      process.exit(1);
    }

    // Hash password
    console.log('\n🔐 Hashing password...');
    const hashedPassword = await hashPassword(password);

    // Update database
    const stmt = db.prepare('UPDATE administrators SET username = ?, password_hash = ? WHERE id = ?');
    stmt.run(username.trim(), hashedPassword, admin.id);

    console.log('✅ Admin password updated successfully!');
    console.log(`   Username: ${username.trim()}`);
    console.log('\nℹ️  Please keep your password secure and do NOT share with anyone.\n');

    rl.close();
    process.exit(0);
  } catch (err) {
    logger.error('Setup failed:', err);
    console.error('❌ Error:', err.message);
    rl.close();
    process.exit(1);
  }
}

setupAdmin();

Part C: Update Technician Service

File: services/techService.js

const db = require('../config/database');
const { hashPassword, verifyPassword } = require('../utils/passwordHelper');

async function authenticate(username, password) {
  const tech = db.prepare('SELECT * FROM technicians WHERE username = ? AND is_active = 1').get(username);
  
  if (!tech) return null;
  
  // Verify password using bcrypt
  const isValid = await verifyPassword(password, tech.password_hash);
  
  if (!isValid) return null;
  
  // Return user data tanpa password
  const { password_hash, ...techData } = tech;
  return techData;
}

async function createTechnician(data) {
  const hashedPassword = await hashPassword(data.password);
  const stmt = db.prepare(
    'INSERT INTO technicians (username, password_hash, name, phone, area) VALUES (?, ?, ?, ?, ?)'
  );
  return stmt.run(data.username, hashedPassword, data.name, data.phone || '', data.area || '');
}

async function updatePassword(techId, newPassword) {
  const hashedPassword = await hashPassword(newPassword);
  return db.prepare('UPDATE technicians SET password_hash = ? WHERE id = ?').run(hashedPassword, techId);
}

module.exports = {
  authenticate,
  createTechnician,
  updatePassword,
  // ... export fungsi lain yang ada
};

Part D: Update Customer Portal Login

File: routes/customerPortal.js (di bagian POST /login)

// Find in the file and replace the login logic:

// ❌ OLD CODE
const cust = db.prepare('SELECT * FROM customers WHERE ...')
  .get(loginId, password);

// ✅ NEW CODE
const cust = db.prepare('SELECT * FROM customers WHERE login_id = ?')
  .get(loginId);

if (cust) {
  const { verifyPassword } = require('../utils/passwordHelper');
  const isValid = await verifyPassword(password, cust.password_hash);
  
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Regenerate session ID untuk security
  req.session.regenerate((err) => {
    if (err) return res.status(500).json({ error: 'Session error' });
    req.session.customerId = cust.id;
    res.json({ success: true });
  });
}

Part E: Database Migration

File: scripts/migrate-password-hashing.js

#!/usr/bin/env node
const db = require('../config/database');
const { hashPassword } = require('../utils/passwordHelper');
const { logger } = require('../config/logger');

async function migrate() {
  console.log('🔄 Starting password hashing migration...\n');

  try {
    // Migrate technicians
    console.log('📋 Migrating technicians...');
    const techs = db.prepare('SELECT id, password FROM technicians WHERE password_hash IS NULL').all();
    
    for (const tech of techs) {
      if (tech.password) {
        const hashedPassword = await hashPassword(tech.password);
        db.prepare('UPDATE technicians SET password_hash = ? WHERE id = ?').run(hashedPassword, tech.id);
        console.log(`  ✅ Tech ${tech.id}`);
      }
    }

    // Migrate customers (if applicable)
    console.log('\n📋 Migrating customers...');
    const custs = db.prepare('SELECT id, password FROM customers WHERE password_hash IS NULL AND password IS NOT NULL').all();
    
    for (const cust of custs) {
      if (cust.password) {
        const hashedPassword = await hashPassword(cust.password);
        db.prepare('UPDATE customers SET password_hash = ? WHERE id = ?').run(hashedPassword, cust.id);
        console.log(`  ✅ Customer ${cust.id}`);
      }
    }

    // Migrate administrators
    console.log('\n📋 Migrating administrators...');
    const admins = db.prepare('SELECT id, password FROM administrators WHERE password_hash IS NULL').all();
    
    for (const admin of admins) {
      if (admin.password) {
        const hashedPassword = await hashPassword(admin.password);
        db.prepare('UPDATE administrators SET password_hash = ? WHERE id = ?').run(hashedPassword, admin.id);
        console.log(`  ✅ Admin ${admin.id}`);
      }
    }

    console.log('\n✅ Migration completed successfully!');
    console.log('\n⚠️  IMPORTANT:');
    console.log('   1. Delete plain password columns: ALTER TABLE technicians DROP COLUMN password;');
    console.log('   2. Ensure all code uses password_hash column');
    console.log('   3. Update settings.json to not store admin password\n');

    logger.info('Password hashing migration completed');
    process.exit(0);
  } catch (err) {
    logger.error('Migration failed:', err);
    console.error('❌ Error:', err.message);
    process.exit(1);
  }
}

migrate();

Part F: Update Database Schema

-- Add password_hash column to tables (if not exists)
ALTER TABLE technicians ADD COLUMN password_hash TEXT;
ALTER TABLE administrators ADD COLUMN password_hash TEXT;
ALTER TABLE customers ADD COLUMN password_hash TEXT;

-- After migration, drop old password columns:
-- ALTER TABLE technicians DROP COLUMN password;
-- ALTER TABLE administrators DROP COLUMN password;
-- ALTER TABLE customers DROP COLUMN password;

🗝️ FIX #2: Environment Variables & Config Management

Part A: Create .env.example

File: .env.example

# Copy this file to .env and fill in actual values
# DO NOT commit .env to git!

# ======== APPLICATION ========
NODE_ENV=development
SERVER_PORT=3001
SERVER_HOST=localhost

# ======== SECURITY ========
SESSION_SECRET=change-me-to-random-string-minimum-32-chars
COOKIE_SECURE=false  # Set to true in production with HTTPS

# ======== ADMIN SETUP ========
# Set these during first-time setup, will be migrated to database
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me-to-strong-password-min-8-chars

# ======== GENIEACS ========
GENIEACS_URL=http://192.168.8.189:7557
GENIEACS_USERNAME=admin
GENIEACS_PASSWORD=change-me

# ======== PAYMENT GATEWAYS ========
MIDTRANS_ENABLED=true
MIDTRANS_SERVER_KEY=Mid_server_xxxxx
MIDTRANS_MODE=sandbox

XENDIT_ENABLED=true
XENDIT_API_KEY=xnd_development_xxxxx

TRIPAY_ENABLED=false
TRIPAY_API_KEY=xxxxx
TRIPAY_PRIVATE_KEY=xxxxx
TRIPAY_MERCHANT_CODE=xxxxx
TRIPAY_MODE=sandbox

DUITKU_ENABLED=true
DUITKU_API_KEY=xxxxx
DUITKU_MERCHANT_CODE=xxxxx
DUITKU_MODE=sandbox

# ======== DIGIFLAZZ ========
DIGIFLAZZ_USERNAME=
DIGIFLAZZ_API_KEY=
DIGIFLAZZ_WEBHOOK_SECRET=

# ======== WHATSAPP ========
WHATSAPP_ENABLED=true
WHATSAPP_AUTH_FOLDER=auth_info_baileys

# ======== TELEGRAM ========
TELEGRAM_ENABLED=false
TELEGRAM_BOT_TOKEN=

# ======== RADIUS ========
RADIUS_ENABLED=0
RADIUS_SECRET=secret123

# ======== LOGGING ========
LOG_LEVEL=info

# ======== DATABASE ========
DATABASE_PATH=./database/billing.db

# ======== TIMEZONE ========
TIMEZONE=Asia/Jakarta

Part B: Update config/settingsManager.js

// Add at the top of settingsManager.js:
require('dotenv').config();

// Modify getSetting function to check .env first:
function getSetting(key, defaultValue = null) {
  // 1. Check environment variable first (highest priority)
  const envKey = key.toUpperCase().replace(/([A-Z])/g, '_$1');
  if (process.env[envKey] !== undefined) {
    return process.env[envKey];
  }

  // 2. Check settings.json
  const settings = getSettings();
  if (settings[key] !== undefined) {
    return settings[key];
  }

  // 3. Return default value
  return defaultValue;
}

Part C: Update .gitignore

# Add to .gitignore:
.env
.env.local
.env.*.local
.env.development.local
.env.staging.local
.env.production.local

# Optional: exclude settings.json if it contains credentials
# settings.json
# Instead: settings.json.example (commit this template)

🛡️ FIX #3: Enhanced CSRF Protection

Part A: Install CSURF

npm install csurf

Part B: Add CSRF to app-customer.js

const csrf = require('csurf');

// Create CSRF protection middleware
const csrfProtection = csrf({ 
  cookie: false  // Use session instead of cookie
});

// Apply to POST/PUT/DELETE routes
app.post('/admin/:path*', csrfProtection, adminRoutes);
app.post('/customer/:path*', csrfProtection, customerRoutes);

Part C: Add CSRF Token to Forms

<!-- In EJS templates, add hidden CSRF token field: -->
<form method="POST" action="/admin/billing/generate">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>">
  <!-- Other form fields -->
  <button type="submit">Generate Invoice</button>
</form>

<!-- For AJAX requests, include in headers: -->
<script>
  const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
  
  fetch('/api/action', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken
    },
    body: JSON.stringify(data)
  });
</script>

🚨 FIX #4: Rate Limiting

Part A: Install & Setup

npm install express-rate-limit

Part B: Create Rate Limiter Middleware

File: middleware/rateLimiter.js

const rateLimit = require('express-rate-limit');

const loginRateLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 5,                     // Limit each IP to 5 requests per windowMs
  message: 'Terlalu banyak percobaan login, coba lagi dalam beberapa menit',
  standardHeaders: true,
  legacyHeaders: false,
});

const apiRateLimiter = rateLimit({
  windowMs: 1 * 60 * 1000,   // 1 minute
  max: 100,                  // 100 requests per minute
  message: 'Terlalu banyak request ke API, coba lagi nanti'
});

const webhookRateLimiter = rateLimit({
  windowMs: 1 * 60 * 1000,
  max: 1000,  // Higher limit for webhooks
  skip: (req) => {
    // Skip rate limiting if webhook signature is valid
    return req.session?.webhookVerified;
  }
});

module.exports = {
  loginRateLimiter,
  apiRateLimiter,
  webhookRateLimiter
};

Part C: Apply Rate Limiters

// In routes
const { loginRateLimiter, apiRateLimiter } = require('../middleware/rateLimiter');

// Apply to login endpoints
router.post('/login', loginRateLimiter, loginController);
router.post('/customer/login', loginRateLimiter, customerLoginController);

// Apply to API endpoints
router.get('/api/data', apiRateLimiter, apiController);

🔒 FIX #5: Session Management Improvements

Part A: Setup Session Store

npm install better-sqlite3-session-store

Part B: Update app-customer.js

const SessionStore = require('better-sqlite3-session-store')(session);

const store = new SessionStore({
  client: db,
  expired: {
    clear: true,
    intervalMs: 15 * 60 * 1000  // Clear expired sessions every 15 minutes
  }
});

app.use(session({
  store: store,  // Use database store instead of memory
  secret: process.env.SESSION_SECRET || getSetting('session_secret'),
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.COOKIE_SECURE === 'true',
    httpOnly: true,
    sameSite: 'strict',  // More restrictive
    maxAge: 24 * 60 * 60 * 1000  // 24 hours
  },
  name: 'customer.sid'
}));

Part C: Session Regeneration on Login

// After successful login:
req.session.regenerate((err) => {
  if (err) {
    return res.status(500).json({ error: 'Session error' });
  }
  req.session.userId = user.id;
  req.session.userRole = user.role;
  req.session.loginTime = Date.now();
  
  // Set inactivity timeout
  req.session.inactivityTimeout = 30 * 60 * 1000;  // 30 minutes
  
  res.json({ success: true, redirect: '/dashboard' });
});

✅ VERIFICATION CHECKLIST

After implementing all fixes, verify:

  • bcrypt password hashing implemented
  • Old plain passwords migrated to hashed
  • .env file created and populated
  • .env added to .gitignore
  • Settings.json reviewed for sensitive data
  • CSRF protection implemented
  • Rate limiting configured
  • Session store using database
  • Session regeneration on login working
  • All tests passing
  • Application running without errors

🧪 TESTING FIXES

Test Password Hashing

node -e "
const { hashPassword, verifyPassword } = require('./utils/passwordHelper');
(async () => {
  const hash = await hashPassword('test123456');
  const valid = await verifyPassword('test123456', hash);
  console.log('Hash:', hash);
  console.log('Valid:', valid);
})();
"

Test Environment Variables

node -e "
require('dotenv').config();
console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('SESSION_SECRET:', process.env.SESSION_SECRET ? '✅ Set' : '❌ Not set');
console.log('GENIEACS_PASSWORD:', process.env.GENIEACS_PASSWORD ? '✅ Set' : '❌ Not set');
"

Test Login Flow

# Start application
npm start

# Open browser and test login with new credentials
# Check console logs for any errors
# Verify session is created in database

📞 TROUBLESHOOTING

Issue: "bcrypt is not defined"

Solution: npm install bcrypt

Issue: ".env file not loading"

Solution: Ensure dotenv is imported at the top of app-customer.js:
require('dotenv').config();

Issue: "CSRF token mismatch"

Solution: Ensure csrfToken is included in all forms and AJAX requests

Issue: "Session not persisting"

Solution: Verify database schema has session table:
sqlite3 database/billing.db ".schema sessions"

📝 NEXT STEPS

  1. Immediate (Today):

    • Review this guide with team
    • Create .env file
    • Test bcrypt locally
  2. This Week:

    • Implement bcrypt in all auth modules
    • Run password migration script
    • Deploy to staging for testing
  3. Next Week:

    • Implement CSRF protection
    • Add rate limiting
    • Full security testing
  4. Following Week:

    • Monitor production (if live)
    • Collect logs and monitoring data
    • Review and optimize

Questions? Review AUDIT_SECURITY_2026.md for detailed security analysis.