PCI DSS Compliance for E-commerce
PCI DSS Compliance for E-commerce
If your e-commerce platform processes, stores, or transmits credit card data, PCI DSS compliance isn’t optional - it’s mandatory. I’ve helped several e-commerce platforms achieve and maintain PCI DSS compliance, and today I’m sharing what I’ve learned.
What is PCI DSS?
PCI DSS (Payment Card Industry Data Security Standard) is a set of security standards designed to ensure that all companies that accept, process, store, or transmit credit card information maintain a secure environment.
Key facts:
- Required by all major card brands (Visa, Mastercard, Amex, Discover)
- Non-compliance can result in fines up to $100,000/month
- Data breaches can cost millions in remediation and lost business
- Applies to merchants, service providers, and payment processors
The 12 PCI DSS Requirements
PCI DSS 4.0 has 12 core requirements across 6 control objectives:
Build and Maintain a Secure Network
1. Install and maintain network security controls 2. Apply secure configurations to all system components
Protect Account Data
3. Protect stored account data 4. Protect cardholder data with strong cryptography during transmission
Maintain a Vulnerability Management Program
5. Protect all systems and networks from malicious software 6. Develop and maintain secure systems and software
Implement Strong Access Control Measures
7. Restrict access to system components and cardholder data by business need-to-know 8. Identify users and authenticate access to system components 9. Restrict physical access to cardholder data
Regularly Monitor and Test Networks
10. Log and monitor all access to system components and cardholder data 11. Test security of systems and networks regularly
Maintain an Information Security Policy
12. Support information security with organizational policies and programs
Let’s implement these for a real e-commerce platform.
Understanding Your SAQ Level
Self-Assessment Questionnaire (SAQ) level depends on how you handle cards:
SAQ A (Easiest)
- E-commerce using fully outsourced payment solution
- No cardholder data on your systems
- Example: Using Stripe Checkout (redirect)
- ~22 requirements
SAQ A-EP
- E-commerce with outsourced payment processing
- Your website hosts payment form
- Example: Stripe Elements embedded
- ~160 requirements
SAQ D (Hardest)
- You process, store, or transmit cardholder data
- Full merchant validation required
- Example: Custom payment processing
- ~300+ requirements
Best practice: Aim for SAQ A whenever possible.
The Smart Architecture: SAQ A Compliance
The easiest path to compliance is not touching card data at all.
Architecture Overview
┌─────────────┐
│ Browser │
└──────┬──────┘
│
│ 1. Customer clicks "Pay"
▼
┌─────────────────┐
│ Your E-commerce │
│ Website │
└──────┬──────────┘
│
│ 2. Redirect to payment provider
▼
┌─────────────────┐
│ Stripe/PayPal │◄── 3. Customer enters card details
│ Hosted Page │
└──────┬──────────┘
│
│ 4. Payment complete, return to site
▼
┌─────────────────┐
│ Your E-commerce │◄── 5. Store payment token, fulfill order
│ Website │
└─────────────────┘
Key: Card data never touches your servers.
Implementation with Stripe
// server.js - Create checkout session
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/create-checkout-session', async (req, res) => {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: req.body.product_name,
},
unit_amount: req.body.amount,
},
quantity: 1,
}],
mode: 'payment',
success_url: `${YOUR_DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${YOUR_DOMAIN}/cancel`,
});
res.json({ url: session.url });
});
// client.js - Redirect to Stripe
async function checkout() {
const response = await fetch('/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
product_name: 'Premium Plan',
amount: 4999, // $49.99
}),
});
const { url } = await response.json();
window.location.href = url; // Redirect to Stripe
}
Result: SAQ A compliance with minimal effort.
For SAQ A-EP: Embedded Payment Forms
If you need payment form on your site (better UX), use tokenization:
Stripe Elements Integration
<!-- checkout.html -->
<!DOCTYPE html>
<html>
<head>
<script src="https://js.stripe.com/v3/"></script>
</head>
<body>
<form id="payment-form">
<div id="card-element"></div>
<button type="submit">Pay $49.99</button>
<div id="error-message"></div>
</form>
<script>
const stripe = Stripe('pk_test_YOUR_PUBLISHABLE_KEY');
const elements = stripe.elements();
// Create card element (Stripe-hosted iframe)
const cardElement = elements.create('card');
cardElement.mount('#card-element');
const form = document.getElementById('payment-form');
form.addEventListener('submit', async (event) => {
event.preventDefault();
// Stripe tokenizes card data
const {error, paymentMethod} = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
});
if (error) {
document.getElementById('error-message').textContent = error.message;
} else {
// Send token to your server (NOT card details)
const response = await fetch('/process-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payment_method_id: paymentMethod.id }),
});
const result = await response.json();
if (result.success) {
window.location.href = '/success';
}
}
});
</script>
</body>
</html>
// server.js - Process tokenized payment
app.post('/process-payment', async (req, res) => {
const { payment_method_id } = req.body;
try {
const paymentIntent = await stripe.paymentIntents.create({
amount: 4999,
currency: 'usd',
payment_method: payment_method_id,
confirm: true,
});
// Store payment intent ID (NOT card details)
await db.orders.create({
user_id: req.user.id,
payment_intent_id: paymentIntent.id,
amount: 4999,
status: 'paid',
});
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Security: Card element is an iframe from Stripe - card data never enters your JavaScript scope.
Requirement 1: Network Security
Firewall Configuration
# iptables rules for e-commerce server
# Default deny
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Allow established connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow SSH (from management network only)
iptables -A INPUT -p tcp --dport 22 -s 10.0.1.0/24 -j ACCEPT
# Allow HTTP/HTTPS
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
# Log dropped packets
iptables -A INPUT -j LOG --log-prefix "DROPPED: "
iptables -A INPUT -j DROP
Network Segmentation
┌──────────────────────────────────────┐
│ DMZ (Public) │
│ ┌──────────┐ ┌──────────┐ │
│ │ Web │ │ Web │ │
│ │ Server 1 │ │ Server 2 │ │
│ └─────┬────┘ └────┬─────┘ │
└────────┼──────────────┼─────────────┘
│ │
┌────┴──────────────┴────┐
│ Load Balancer │
└────────┬───────────────┘
│
┌────────────┼────────────────────────┐
│ Internal Network │
│ ┌───────┴────┐ ┌───────────┐ │
│ │ App │────▶│ Database │ │
│ │ Servers │ │ (Encrypted)│ │
│ └────────────┘ └───────────┘ │
└────────────────────────────────────┘
│
┌────────┴──────────────────┐
│ Management Network │
│ (Bastion/Jump Server) │
└───────────────────────────┘
Key principles:
- Public-facing web servers in DMZ
- Application servers in internal network
- Database servers in most protected zone
- Management access via jump server only
Requirement 3: Protect Stored Data
What You Can Store
Allowed (after transaction):
- Primary Account Number (PAN) - encrypted
- Cardholder name
- Expiration date
- Service code
Never allowed:
- Full magnetic stripe data
- CAV2/CVC2/CVV2/CID
- PIN/PIN block
Encryption at Rest
// Encrypt sensitive data before storing
const crypto = require('crypto');
class DataProtection {
constructor() {
// AES-256-GCM encryption
this.algorithm = 'aes-256-gcm';
this.key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
}
encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Store: iv + authTag + encrypted
return iv.toString('hex') + authTag.toString('hex') + encrypted;
}
decrypt(encryptedData) {
const iv = Buffer.from(encryptedData.slice(0, 32), 'hex');
const authTag = Buffer.from(encryptedData.slice(32, 64), 'hex');
const encrypted = encryptedData.slice(64);
const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
// Usage
const dp = new DataProtection();
// Store last 4 digits only, encrypted
const last4 = cardNumber.slice(-4);
const encryptedLast4 = dp.encrypt(last4);
await db.customers.update({
card_last4: encryptedLast4,
card_brand: 'Visa',
card_exp_month: 12,
card_exp_year: 2025,
});
Best practice: Don’t store PAN at all. Use payment provider tokens.
Requirement 4: Encryption in Transit
TLS Configuration
# nginx.conf - A+ SSL Labs rating
server {
listen 443 ssl http2;
server_name shop.example.com;
# Certificates
ssl_certificate /etc/letsencrypt/live/shop.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem;
# Strong ciphers only
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/shop.example.com/chain.pem;
# Session settings
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
location / {
proxy_pass http://app_servers;
proxy_set_header X-Forwarded-Proto https;
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name shop.example.com;
return 301 https://$server_name$request_uri;
}
Database Connection Encryption
// PostgreSQL with SSL
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: {
rejectUnauthorized: true,
ca: fs.readFileSync('/path/to/ca-cert.pem'),
cert: fs.readFileSync('/path/to/client-cert.pem'),
key: fs.readFileSync('/path/to/client-key.pem'),
},
});
Requirement 6: Secure Development
Code Security Scanning
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# SAST - Static Application Security Testing
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: p/owasp-top-ten
# Dependency scanning
- name: Run npm audit
run: npm audit --audit-level=high
# Secret scanning
- name: TruffleHog Secret Scan
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: main
head: HEAD
# Container scanning
- name: Build and scan Docker image
run: |
docker build -t myapp:${{ github.sha }} .
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image --severity HIGH,CRITICAL myapp:${{ github.sha }}
Secure Coding Practices
// Input validation
const Joi = require('joi');
const orderSchema = Joi.object({
product_id: Joi.string().uuid().required(),
quantity: Joi.number().integer().min(1).max(100).required(),
shipping_address: Joi.object({
street: Joi.string().max(100).required(),
city: Joi.string().max(50).required(),
state: Joi.string().length(2).required(),
zip: Joi.string().pattern(/^\d{5}(-\d{4})?$/).required(),
}).required(),
});
app.post('/orders', async (req, res) => {
// Validate input
const { error, value } = orderSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
// Parameterized queries (prevent SQL injection)
const result = await db.query(
'INSERT INTO orders (user_id, product_id, quantity) VALUES ($1, $2, $3) RETURNING id',
[req.user.id, value.product_id, value.quantity]
);
res.json({ order_id: result.rows[0].id });
});
Requirement 8: Access Control
Multi-Factor Authentication
const speakeasy = require('speakeasy');
const qrcode = require('qrcode');
// Enable MFA for admin users
app.post('/admin/mfa/setup', async (req, res) => {
const secret = speakeasy.generateSecret({
name: `Shop (${req.user.email})`,
issuer: 'MyShop',
});
// Store encrypted secret
await db.users.update({
where: { id: req.user.id },
data: { mfa_secret: encrypt(secret.base32) },
});
// Generate QR code
const qrCodeUrl = await qrcode.toDataURL(secret.otpauth_url);
res.json({ qrCode: qrCodeUrl });
});
// Verify MFA token
app.post('/admin/mfa/verify', async (req, res) => {
const user = await db.users.findUnique({ where: { id: req.user.id } });
const secret = decrypt(user.mfa_secret);
const verified = speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: req.body.token,
window: 2, // Allow 2 time steps (60 seconds)
});
if (verified) {
req.session.mfa_verified = true;
res.json({ success: true });
} else {
res.status(401).json({ error: 'Invalid MFA token' });
}
});
Role-Based Access Control
// Permission middleware
const permissions = {
admin: ['read', 'write', 'delete', 'manage_users'],
manager: ['read', 'write'],
support: ['read'],
};
function requirePermission(action) {
return (req, res, next) => {
const userPermissions = permissions[req.user.role] || [];
if (!userPermissions.includes(action)) {
return res.status(403).json({ error: 'Permission denied' });
}
next();
};
}
// Usage
app.get('/admin/orders', requirePermission('read'), async (req, res) => {
const orders = await db.orders.findMany();
res.json(orders);
});
app.delete('/admin/orders/:id', requirePermission('delete'), async (req, res) => {
await db.orders.delete({ where: { id: req.params.id } });
res.json({ success: true });
});
Requirement 10: Logging and Monitoring
Comprehensive Audit Logging
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'audit.log' }),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
],
});
// Audit middleware
function auditLog(action) {
return (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info({
action,
user_id: req.user?.id,
ip: req.ip,
method: req.method,
path: req.path,
status: res.statusCode,
duration_ms: Date.now() - start,
user_agent: req.headers['user-agent'],
});
});
next();
};
}
// Usage
app.post('/orders', auditLog('create_order'), async (req, res) => {
// Create order
});
app.delete('/admin/users/:id', auditLog('delete_user'), async (req, res) => {
// Delete user
});
Security Event Monitoring
// Alert on suspicious activity
const alertThresholds = {
failed_logins: 5,
time_window_minutes: 15,
};
async function checkSecurityEvents() {
const recentFailedLogins = await db.auditLog.count({
where: {
action: 'failed_login',
created_at: {
gte: new Date(Date.now() - alertThresholds.time_window_minutes * 60000),
},
},
groupBy: ['ip'],
});
for (const [ip, count] of Object.entries(recentFailedLogins)) {
if (count >= alertThresholds.failed_logins) {
await sendAlert({
type: 'brute_force_attempt',
ip,
failed_attempts: count,
action: 'IP temporarily blocked',
});
// Block IP
await firewall.blockIP(ip, '1 hour');
}
}
}
// Run every 5 minutes
setInterval(checkSecurityEvents, 5 * 60 * 1000);
Requirement 11: Security Testing
Vulnerability Scanning
#!/bin/bash
# weekly-scan.sh
# Web application scan
nikto -h https://shop.example.com -output nikto-report.html
# SSL/TLS scan
testssl.sh --severity HIGH https://shop.example.com
# Port scan
nmap -sV -sC shop.example.com
# Dependency vulnerabilities
npm audit --audit-level=high
Penetration Testing
Required frequency: Annually + after significant changes
Test areas:
- Authentication and session management
- SQL injection
- Cross-site scripting (XSS)
- Cross-site request forgery (CSRF)
- Insecure direct object references
- Security misconfigurations
- Sensitive data exposure
PCI DSS Compliance Checklist
Before Going Live
- Network firewall configured
- Systems hardened (unnecessary services disabled)
- TLS 1.2+ enforced
- Cardholder data encrypted at rest
- Antivirus installed and updated
- Secure development practices implemented
- Access control implemented (RBAC + MFA)
- Unique user IDs for all personnel
- Physical access controls (if applicable)
- Logging enabled for all systems
- Security testing completed
- Information security policy documented
Monthly
- Review access logs
- Review firewall rules
- Antivirus definitions updated
- Security patches applied
- User access reviewed
Quarterly
- Vulnerability scans (by ASV)
- Review security policies
- Security awareness training
Annually
- Complete SAQ
- Penetration testing
- Attestation of Compliance (AOC)
- Policy review and updates
Common Compliance Pitfalls
- Storing full PAN - Use tokenization instead
- Weak passwords - Enforce strong password policy
- No MFA - Require for admin access
- Logging disabled - Enable comprehensive logging
- Missing patches - Automate patch management
- Default credentials - Change all defaults
- No network segmentation - Isolate CDE
- Insufficient testing - Regular vulnerability scans
Tools for Compliance
Network Security:
- pfSense - Firewall
- Suricata - IDS/IPS
- Wireshark - Network analysis
Vulnerability Management:
- Nessus - Vulnerability scanner
- OpenVAS - Open source scanner
- Qualys - Cloud-based scanning
Log Management:
- ELK Stack - Log aggregation
- Splunk - SIEM platform
- Graylog - Log management
Compliance Management:
- SecurityMetrics - PCI compliance platform
- TrustArc - Privacy and compliance
- Vanta - Automated compliance
Key Takeaways
- Outsource payment processing - SAQ A is easiest path
- Never store full PAN - Use tokenization
- Encrypt everything - Data at rest and in transit
- Log everything - Comprehensive audit trails
- Test regularly - Vulnerability scans and pentests
- Train your team - Security awareness is critical
- Document everything - Policies, procedures, evidence
Resources
- PCI Security Standards Council
- PCI DSS Quick Reference Guide
- Stripe PCI Compliance Guide
- OWASP Payment Card Industry
Conclusion
PCI DSS compliance doesn’t have to be overwhelming. The key is architecting your system to minimize cardholder data exposure. By outsourcing payment processing to compliant providers like Stripe or PayPal, you can achieve compliance with minimal effort.
For businesses that must handle card data directly, implementing the 12 requirements systematically - with proper network segmentation, encryption, access controls, and monitoring - will get you there.
Remember: Compliance is ongoing, not a one-time checklist. Regular testing, monitoring, and updates are essential to maintaining your compliant status.
Published: November 15, 2025