@xenterprises/fastify-xtwilio 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +63 -0
- package/.env.example +257 -0
- package/API.md +973 -0
- package/CHANGELOG.md +189 -0
- package/LICENSE +15 -0
- package/README.md +261 -0
- package/SECURITY.md +721 -0
- package/index.d.ts +999 -0
- package/package.json +55 -0
- package/server/app.js +88 -0
- package/src/services/conversations.js +328 -0
- package/src/services/email.js +362 -0
- package/src/services/rcs.js +284 -0
- package/src/services/sms.js +268 -0
- package/src/xTwilio.js +37 -0
- package/test/xTwilio.test.js +511 -0
package/SECURITY.md
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
# Security Guidelines for xTwilio
|
|
2
|
+
|
|
3
|
+
This document outlines security best practices for using the xTwilio plugin to protect your communications infrastructure, API credentials, and customer data.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Credential Management](#credential-management)
|
|
8
|
+
2. [API Key Security](#api-key-security)
|
|
9
|
+
3. [Webhook Security](#webhook-security)
|
|
10
|
+
4. [Input Validation](#input-validation)
|
|
11
|
+
5. [Phone Number Handling](#phone-number-handling)
|
|
12
|
+
6. [Email Security](#email-security)
|
|
13
|
+
7. [Error Handling](#error-handling)
|
|
14
|
+
8. [Rate Limiting](#rate-limiting)
|
|
15
|
+
9. [Compliance & Privacy](#compliance--privacy)
|
|
16
|
+
10. [Incident Response](#incident-response)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. Credential Management
|
|
21
|
+
|
|
22
|
+
### Never Hardcode Credentials
|
|
23
|
+
|
|
24
|
+
Always use environment variables for sensitive credentials. Never commit API keys, auth tokens, or secrets to version control.
|
|
25
|
+
|
|
26
|
+
**❌ WRONG:**
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
const fastify = await fastify.register(xTwilio, {
|
|
30
|
+
twilio: {
|
|
31
|
+
accountSid: 'AC1234567890abcdef1234567890abcdef',
|
|
32
|
+
authToken: 'your_secret_auth_token_here',
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**✅ CORRECT:**
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
const fastify = await fastify.register(xTwilio, {
|
|
41
|
+
twilio: {
|
|
42
|
+
accountSid: process.env.TWILIO_ACCOUNT_SID,
|
|
43
|
+
authToken: process.env.TWILIO_AUTH_TOKEN,
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Environment Variable Management
|
|
49
|
+
|
|
50
|
+
1. Create a `.env` file locally (never commit to Git)
|
|
51
|
+
2. Use `.env.example` as a template for developers
|
|
52
|
+
3. In production, set environment variables through your hosting platform:
|
|
53
|
+
- **Vercel**: Project Settings → Environment Variables
|
|
54
|
+
- **AWS**: Systems Manager → Parameter Store or Secrets Manager
|
|
55
|
+
- **Docker**: Use Docker secrets or `.env` files in secure storage
|
|
56
|
+
- **Heroku**: Config Vars in app settings
|
|
57
|
+
|
|
58
|
+
### Rotating Credentials
|
|
59
|
+
|
|
60
|
+
Regularly rotate API keys and auth tokens:
|
|
61
|
+
|
|
62
|
+
1. Generate new credentials in Twilio/SendGrid dashboards
|
|
63
|
+
2. Update environment variables in production
|
|
64
|
+
3. Test with new credentials before removing old ones
|
|
65
|
+
4. Revoke old credentials once new ones are verified
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 2. API Key Security
|
|
70
|
+
|
|
71
|
+
### Twilio Credentials
|
|
72
|
+
|
|
73
|
+
Your Twilio Account SID and Auth Token provide full access to your Twilio account. Treat them like passwords.
|
|
74
|
+
|
|
75
|
+
**Protection Level: CRITICAL**
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Store in a secure vault, not in code or config files
|
|
79
|
+
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 34 chars
|
|
80
|
+
TWILIO_AUTH_TOKEN=your_secret_token_here # Very long string
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### SendGrid API Key
|
|
84
|
+
|
|
85
|
+
SendGrid API keys have full API access to your account.
|
|
86
|
+
|
|
87
|
+
**Protection Level: CRITICAL**
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Always use the least-privileged API key
|
|
91
|
+
# Create keys in SendGrid dashboard with specific permissions
|
|
92
|
+
SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### SendGrid Key Permissions
|
|
96
|
+
|
|
97
|
+
When creating API keys, enable only the permissions you need:
|
|
98
|
+
|
|
99
|
+
- **For sending only**: Enable `mail.send` only
|
|
100
|
+
- **For contact management**: Enable `contacts.read`, `contacts.write`
|
|
101
|
+
- **For list management**: Enable `lists.read`, `lists.write`
|
|
102
|
+
- **For templates**: Enable `templates.read`
|
|
103
|
+
|
|
104
|
+
Example: Key for sending emails only
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
Mail Send: Full Access
|
|
108
|
+
Contacts: None
|
|
109
|
+
Lists: None
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Logging & Exposure
|
|
113
|
+
|
|
114
|
+
Never log credentials in any form:
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
// ❌ WRONG - Exposes API key in logs
|
|
118
|
+
console.log('Config:', {
|
|
119
|
+
twilioAccountSid: process.env.TWILIO_ACCOUNT_SID,
|
|
120
|
+
twilioAuthToken: process.env.TWILIO_AUTH_TOKEN, // Exposed!
|
|
121
|
+
sendgridApiKey: process.env.SENDGRID_API_KEY, // Exposed!
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ✅ CORRECT - Safe logging with masking
|
|
125
|
+
console.log('Config loaded:', {
|
|
126
|
+
twilio: 'configured',
|
|
127
|
+
sendgrid: 'configured',
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 3. Webhook Security
|
|
134
|
+
|
|
135
|
+
### Webhook Signature Validation
|
|
136
|
+
|
|
137
|
+
Always validate Twilio webhooks to ensure they come from Twilio:
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
import twilio from 'twilio';
|
|
141
|
+
|
|
142
|
+
// Verify webhook signature
|
|
143
|
+
const isValidRequest = twilio.validateRequest(
|
|
144
|
+
process.env.TWILIO_AUTH_TOKEN,
|
|
145
|
+
req.headers['x-twilio-signature'],
|
|
146
|
+
'https://yourdomain.com/webhooks/twilio',
|
|
147
|
+
req.rawBody
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (!isValidRequest) {
|
|
151
|
+
return reply.code(403).send({ error: 'Invalid signature' });
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Webhook Configuration
|
|
156
|
+
|
|
157
|
+
1. Use HTTPS endpoints only (never HTTP in production)
|
|
158
|
+
2. Set strong webhook secrets:
|
|
159
|
+
```bash
|
|
160
|
+
TWILIO_CONVERSATIONS_WEBHOOK_SECRET=your_strong_random_secret
|
|
161
|
+
```
|
|
162
|
+
3. Validate signatures in all webhook handlers
|
|
163
|
+
4. Implement request logging (without exposing sensitive data)
|
|
164
|
+
|
|
165
|
+
### Webhook URL Security
|
|
166
|
+
|
|
167
|
+
- Use unique paths: `/webhooks/twilio` not just `/webhook`
|
|
168
|
+
- Implement IP allowlisting if possible (restrict to Twilio IPs)
|
|
169
|
+
- Require authentication tokens in webhook headers
|
|
170
|
+
- Implement rate limiting on webhook endpoints
|
|
171
|
+
|
|
172
|
+
**Example Secure Webhook Handler:**
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
import crypto from 'crypto';
|
|
176
|
+
|
|
177
|
+
fastify.post('/webhooks/twilio', async (request, reply) => {
|
|
178
|
+
// 1. Validate signature
|
|
179
|
+
const signature = request.headers['x-twilio-signature'];
|
|
180
|
+
const expectedSignature = crypto
|
|
181
|
+
.createHmac('sha1', process.env.TWILIO_AUTH_TOKEN)
|
|
182
|
+
.update(`https://yourdomain.com/webhooks/twilio${JSON.stringify(request.body)}`)
|
|
183
|
+
.digest('base64');
|
|
184
|
+
|
|
185
|
+
if (signature !== expectedSignature) {
|
|
186
|
+
fastify.log.error('Invalid webhook signature');
|
|
187
|
+
return reply.code(403).send({ error: 'Unauthorized' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 2. Process webhook
|
|
191
|
+
const { MessageSid, From, Body } = request.body;
|
|
192
|
+
|
|
193
|
+
// 3. Don't expose sensitive data in response
|
|
194
|
+
return reply.code(200).send({ status: 'received' });
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 4. Input Validation
|
|
201
|
+
|
|
202
|
+
### Phone Number Validation
|
|
203
|
+
|
|
204
|
+
Always validate phone numbers before sending SMS:
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
// ✅ CORRECT - Validate phone number
|
|
208
|
+
const validation = await fastify.sms.validatePhoneNumber(phoneNumber, {
|
|
209
|
+
countryCode: 'US'
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (!validation.isValid) {
|
|
213
|
+
return reply.code(400).send({
|
|
214
|
+
error: 'Invalid phone number',
|
|
215
|
+
details: validation.error
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await fastify.sms.send(validation.phoneNumber, message);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Message Content Validation
|
|
223
|
+
|
|
224
|
+
Validate message content to prevent injection attacks:
|
|
225
|
+
|
|
226
|
+
```javascript
|
|
227
|
+
// ❌ WRONG - Accepts arbitrary input
|
|
228
|
+
await fastify.sms.send(phoneNumber, userInput);
|
|
229
|
+
|
|
230
|
+
// ✅ CORRECT - Validates message content
|
|
231
|
+
function validateMessage(message) {
|
|
232
|
+
if (!message || typeof message !== 'string') {
|
|
233
|
+
throw new Error('Message must be a non-empty string');
|
|
234
|
+
}
|
|
235
|
+
if (message.length > 160) {
|
|
236
|
+
throw new Error('Message exceeds SMS length limit');
|
|
237
|
+
}
|
|
238
|
+
// Remove potentially harmful characters
|
|
239
|
+
return message.replace(/[<>\"']/g, '');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const validMessage = validateMessage(userInput);
|
|
243
|
+
await fastify.sms.send(phoneNumber, validMessage);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Email Input Validation
|
|
247
|
+
|
|
248
|
+
Validate email addresses before sending:
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
// Use a robust email validation library
|
|
252
|
+
import { validateEmail } from 'email-validator';
|
|
253
|
+
|
|
254
|
+
const isValid = validateEmail(userEmail);
|
|
255
|
+
if (!isValid) {
|
|
256
|
+
return reply.code(400).send({ error: 'Invalid email address' });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Or validate through SendGrid
|
|
260
|
+
const validation = await fastify.email.validate(userEmail);
|
|
261
|
+
if (!validation.isValid) {
|
|
262
|
+
return reply.code(400).send({ error: 'Email address not deliverable' });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await fastify.email.send(userEmail, subject, html);
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 5. Phone Number Handling
|
|
271
|
+
|
|
272
|
+
### Prevent SMS Bombing
|
|
273
|
+
|
|
274
|
+
Implement rate limiting to prevent SMS abuse:
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
import rateLimit from '@fastify/rate-limit';
|
|
278
|
+
|
|
279
|
+
// Add rate limiting plugin
|
|
280
|
+
await fastify.register(rateLimit, {
|
|
281
|
+
max: 10, // Maximum 10 requests
|
|
282
|
+
timeWindow: '15 minutes'
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
fastify.post('/send-sms', async (request, reply) => {
|
|
286
|
+
const { phoneNumber, message } = request.body;
|
|
287
|
+
|
|
288
|
+
// Check rate limit (automatic with plugin)
|
|
289
|
+
|
|
290
|
+
await fastify.sms.send(phoneNumber, message);
|
|
291
|
+
return { status: 'sent' };
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Phone Number Privacy
|
|
296
|
+
|
|
297
|
+
Don't expose full phone numbers in logs or responses:
|
|
298
|
+
|
|
299
|
+
```javascript
|
|
300
|
+
// ❌ WRONG - Exposes full phone number
|
|
301
|
+
fastify.log.info('SMS sent to:', phoneNumber);
|
|
302
|
+
|
|
303
|
+
// ✅ CORRECT - Masks phone number
|
|
304
|
+
function maskPhoneNumber(phone) {
|
|
305
|
+
const last4 = phone.slice(-4);
|
|
306
|
+
const masked = '*'.repeat(phone.length - 4) + last4;
|
|
307
|
+
return masked;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fastify.log.info('SMS sent to:', maskPhoneNumber(phoneNumber));
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### International Phone Numbers
|
|
314
|
+
|
|
315
|
+
Handle international numbers securely:
|
|
316
|
+
|
|
317
|
+
```javascript
|
|
318
|
+
// Use Twilio's Lookup API to validate international numbers
|
|
319
|
+
const validation = await fastify.sms.validatePhoneNumber(
|
|
320
|
+
phoneNumber,
|
|
321
|
+
{
|
|
322
|
+
type: 'carrier',
|
|
323
|
+
countryCode: 'FR' // Specify country for context
|
|
324
|
+
}
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// Check for expensive operations (video calls, international rates)
|
|
328
|
+
if (validation.carrier && validation.carrierType === 'voip') {
|
|
329
|
+
// Handle VOIP numbers differently if needed
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## 6. Email Security
|
|
336
|
+
|
|
337
|
+
### Prevent Email Header Injection
|
|
338
|
+
|
|
339
|
+
Sanitize email headers to prevent injection attacks:
|
|
340
|
+
|
|
341
|
+
```javascript
|
|
342
|
+
// ❌ WRONG - Unsafe headers
|
|
343
|
+
await fastify.email.send(
|
|
344
|
+
userEmail,
|
|
345
|
+
userSubject, // User input in header!
|
|
346
|
+
html
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// ✅ CORRECT - Sanitized headers
|
|
350
|
+
function sanitizeHeader(header) {
|
|
351
|
+
// Remove newlines, carriage returns, and other injection vectors
|
|
352
|
+
return header.replace(/[\r\n]/g, '');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const safeSubject = sanitizeHeader(userSubject);
|
|
356
|
+
await fastify.email.send(userEmail, safeSubject, html);
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Attachment Security
|
|
360
|
+
|
|
361
|
+
Be careful with email attachments:
|
|
362
|
+
|
|
363
|
+
```javascript
|
|
364
|
+
// ❌ WRONG - Accepts any file
|
|
365
|
+
await fastify.email.sendWithAttachments(
|
|
366
|
+
userEmail,
|
|
367
|
+
subject,
|
|
368
|
+
html,
|
|
369
|
+
userProvidedAttachments // Dangerous!
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// ✅ CORRECT - Validates attachments
|
|
373
|
+
function validateAttachment(attachment) {
|
|
374
|
+
const MAX_SIZE = 25 * 1024 * 1024; // 25 MB
|
|
375
|
+
const ALLOWED_TYPES = [
|
|
376
|
+
'application/pdf',
|
|
377
|
+
'image/jpeg',
|
|
378
|
+
'image/png',
|
|
379
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
if (Buffer.byteLength(attachment.content, 'base64') > MAX_SIZE) {
|
|
383
|
+
throw new Error('Attachment too large');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (!ALLOWED_TYPES.includes(attachment.type)) {
|
|
387
|
+
throw new Error('File type not allowed');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return attachment;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const validAttachments = userAttachments.map(validateAttachment);
|
|
394
|
+
await fastify.email.sendWithAttachments(userEmail, subject, html, validAttachments);
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Unsubscribe Links
|
|
398
|
+
|
|
399
|
+
Always include unsubscribe functionality in marketing emails:
|
|
400
|
+
|
|
401
|
+
```javascript
|
|
402
|
+
const htmlWithUnsubscribe = `
|
|
403
|
+
${htmlContent}
|
|
404
|
+
<p>
|
|
405
|
+
<a href="https://yourdomain.com/unsubscribe?id=${userId}">Unsubscribe</a>
|
|
406
|
+
</p>
|
|
407
|
+
`;
|
|
408
|
+
|
|
409
|
+
await fastify.email.send(userEmail, subject, htmlWithUnsubscribe);
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### SPF, DKIM, and DMARC Configuration
|
|
413
|
+
|
|
414
|
+
Set up email authentication in your SendGrid account:
|
|
415
|
+
|
|
416
|
+
1. **SPF (Sender Policy Framework)**
|
|
417
|
+
- Add SPF record to DNS: `v=spf1 sendgrid.net ~all`
|
|
418
|
+
|
|
419
|
+
2. **DKIM (DomainKeys Identified Mail)**
|
|
420
|
+
- Enable DKIM in SendGrid dashboard
|
|
421
|
+
- Add DKIM records to DNS
|
|
422
|
+
|
|
423
|
+
3. **DMARC (Domain-based Message Authentication)**
|
|
424
|
+
- Set DMARC policy: `v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com`
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## 7. Error Handling
|
|
429
|
+
|
|
430
|
+
### Don't Expose Sensitive Data in Errors
|
|
431
|
+
|
|
432
|
+
Never include credentials, tokens, or full API responses in error messages:
|
|
433
|
+
|
|
434
|
+
```javascript
|
|
435
|
+
// ❌ WRONG - Exposes sensitive information
|
|
436
|
+
fastify.post('/send-sms', async (request, reply) => {
|
|
437
|
+
try {
|
|
438
|
+
await fastify.sms.send(phoneNumber, message);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
fastify.log.error('Full error:', error); // Logs sensitive details!
|
|
441
|
+
return reply.code(500).send(error); // Sends raw error to client!
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// ✅ CORRECT - Safe error handling
|
|
446
|
+
fastify.post('/send-sms', async (request, reply) => {
|
|
447
|
+
try {
|
|
448
|
+
await fastify.sms.send(phoneNumber, message);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
// Log full error internally for debugging
|
|
451
|
+
fastify.log.error('SMS send failed', {
|
|
452
|
+
code: error.code,
|
|
453
|
+
status: error.status,
|
|
454
|
+
// Don't log: message, accountSid, authToken
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Return safe error to client
|
|
458
|
+
if (error.status === 400) {
|
|
459
|
+
return reply.code(400).send({
|
|
460
|
+
error: 'Invalid phone number or message'
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return reply.code(500).send({
|
|
465
|
+
error: 'Failed to send message. Please try again later.'
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Structured Error Logging
|
|
472
|
+
|
|
473
|
+
Use structured logging to capture errors without exposing secrets:
|
|
474
|
+
|
|
475
|
+
```javascript
|
|
476
|
+
function logError(context, error) {
|
|
477
|
+
fastify.log.error({
|
|
478
|
+
timestamp: new Date().toISOString(),
|
|
479
|
+
context,
|
|
480
|
+
errorCode: error.code,
|
|
481
|
+
errorStatus: error.status,
|
|
482
|
+
message: 'Safe error description here',
|
|
483
|
+
// Never include:
|
|
484
|
+
// - authToken
|
|
485
|
+
// - apiKey
|
|
486
|
+
// - phoneNumber (full)
|
|
487
|
+
// - email addresses
|
|
488
|
+
// - rawErrorMessage
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## 8. Rate Limiting
|
|
496
|
+
|
|
497
|
+
### SMS Rate Limiting
|
|
498
|
+
|
|
499
|
+
Prevent SMS spam with rate limiting:
|
|
500
|
+
|
|
501
|
+
```javascript
|
|
502
|
+
// Configure in .env
|
|
503
|
+
SMS_RATE_LIMIT_PER_MINUTE=10
|
|
504
|
+
|
|
505
|
+
// Implement in your code
|
|
506
|
+
async function sendSMS(phoneNumber, message) {
|
|
507
|
+
// Check rate limit for this phone number
|
|
508
|
+
const key = `sms:${phoneNumber}`;
|
|
509
|
+
const count = await fastify.redis.incr(key);
|
|
510
|
+
|
|
511
|
+
if (count === 1) {
|
|
512
|
+
// Reset counter every minute
|
|
513
|
+
await fastify.redis.expire(key, 60);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (count > parseInt(process.env.SMS_RATE_LIMIT_PER_MINUTE || '10')) {
|
|
517
|
+
throw new Error('Rate limit exceeded for this phone number');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return await fastify.sms.send(phoneNumber, message);
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Email Rate Limiting
|
|
525
|
+
|
|
526
|
+
Prevent email abuse:
|
|
527
|
+
|
|
528
|
+
```javascript
|
|
529
|
+
// Configure in .env
|
|
530
|
+
EMAIL_RATE_LIMIT_PER_MINUTE=5
|
|
531
|
+
|
|
532
|
+
// Implement in your code
|
|
533
|
+
async function sendEmail(recipientEmail, subject, html) {
|
|
534
|
+
const key = `email:${recipientEmail}`;
|
|
535
|
+
const count = await fastify.redis.incr(key);
|
|
536
|
+
|
|
537
|
+
if (count === 1) {
|
|
538
|
+
await fastify.redis.expire(key, 60);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (count > parseInt(process.env.EMAIL_RATE_LIMIT_PER_MINUTE || '5')) {
|
|
542
|
+
throw new Error('Rate limit exceeded for this recipient');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return await fastify.email.send(recipientEmail, subject, html);
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## 9. Compliance & Privacy
|
|
552
|
+
|
|
553
|
+
### GDPR Compliance
|
|
554
|
+
|
|
555
|
+
- **Right to Access**: Maintain records of messages sent to users
|
|
556
|
+
- **Right to Erasure**: Delete user data upon request
|
|
557
|
+
- **Right to Rectification**: Allow users to update contact information
|
|
558
|
+
- **Consent**: Maintain proof of user consent before sending marketing emails
|
|
559
|
+
|
|
560
|
+
```javascript
|
|
561
|
+
// Example: Request to delete user data
|
|
562
|
+
fastify.delete('/api/users/:userId/data', async (request, reply) => {
|
|
563
|
+
const { userId } = request.params;
|
|
564
|
+
|
|
565
|
+
// Delete from your database
|
|
566
|
+
await db.users.delete(userId);
|
|
567
|
+
|
|
568
|
+
// Request deletion from Twilio (contact Twilio support)
|
|
569
|
+
// Request deletion from SendGrid contacts
|
|
570
|
+
await fastify.email.deleteContact(userId);
|
|
571
|
+
|
|
572
|
+
return { status: 'deleted' };
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### SMS Compliance (TCPA)
|
|
577
|
+
|
|
578
|
+
**In the United States**, SMS messages are regulated by TCPA:
|
|
579
|
+
|
|
580
|
+
- **Consent**: Obtain explicit written consent before sending SMS
|
|
581
|
+
- **Opt-out**: Honor opt-out requests immediately
|
|
582
|
+
- **Headers**: Include sender identification
|
|
583
|
+
- **Timing**: Don't send SMS between 9 PM and 8 AM recipient's time zone
|
|
584
|
+
|
|
585
|
+
```javascript
|
|
586
|
+
// Check consent before sending
|
|
587
|
+
async function sendCompliantSMS(phoneNumber, message) {
|
|
588
|
+
const user = await db.users.findByPhone(phoneNumber);
|
|
589
|
+
|
|
590
|
+
if (!user.smsConsent) {
|
|
591
|
+
throw new Error('User has not consented to SMS');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (user.unsubscribedFromSMS) {
|
|
595
|
+
throw new Error('User has opted out of SMS');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return await fastify.sms.send(phoneNumber, message);
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Email Compliance (CAN-SPAM)
|
|
603
|
+
|
|
604
|
+
**In the United States**, email marketing is regulated by CAN-SPAM:
|
|
605
|
+
|
|
606
|
+
- **Subject**: Must not be deceptive
|
|
607
|
+
- **Identification**: Identify message as advertisement
|
|
608
|
+
- **Reply Address**: Provide valid reply-to address
|
|
609
|
+
- **Unsubscribe**: Honor unsubscribe requests within 10 business days
|
|
610
|
+
|
|
611
|
+
```javascript
|
|
612
|
+
// Send CAN-SPAM compliant email
|
|
613
|
+
async function sendMarketingEmail(to, subject, html) {
|
|
614
|
+
const complianceFooter = `
|
|
615
|
+
<footer style="margin-top: 2rem; padding: 1rem; border-top: 1px solid #ddd; font-size: 0.8rem;">
|
|
616
|
+
<p>Your Company Name<br/>
|
|
617
|
+
123 Business St<br/>
|
|
618
|
+
City, ST 12345</p>
|
|
619
|
+
<p><a href="https://yourdomain.com/unsubscribe?email=${encodeURIComponent(to)}">
|
|
620
|
+
Unsubscribe from this mailing list
|
|
621
|
+
</a></p>
|
|
622
|
+
</footer>
|
|
623
|
+
`;
|
|
624
|
+
|
|
625
|
+
return await fastify.email.send(
|
|
626
|
+
to,
|
|
627
|
+
`[ADVERTISEMENT] ${subject}`,
|
|
628
|
+
`${html}${complianceFooter}`,
|
|
629
|
+
{ replyTo: 'support@yourdomain.com' }
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## 10. Incident Response
|
|
637
|
+
|
|
638
|
+
### What To Do If Credentials Are Exposed
|
|
639
|
+
|
|
640
|
+
1. **Immediately Revoke Credentials**
|
|
641
|
+
- Twilio: https://console.twilio.com/ → Account → Security → Auth Tokens
|
|
642
|
+
- SendGrid: https://app.sendgrid.com/settings/api_keys
|
|
643
|
+
- Delete the exposed credentials
|
|
644
|
+
|
|
645
|
+
2. **Generate New Credentials**
|
|
646
|
+
- Create new Auth Token/API Key in the respective dashboard
|
|
647
|
+
- Update all environment variables
|
|
648
|
+
|
|
649
|
+
3. **Audit Recent Activity**
|
|
650
|
+
- Check Twilio audit logs for unauthorized API calls
|
|
651
|
+
- Check SendGrid logs for unauthorized email sends
|
|
652
|
+
- Review SMS/call logs for suspicious activity
|
|
653
|
+
|
|
654
|
+
4. **Notify Users (If Applicable)**
|
|
655
|
+
- If user data was accessed, notify affected users
|
|
656
|
+
- Document the breach for regulatory reporting
|
|
657
|
+
|
|
658
|
+
5. **Update Infrastructure**
|
|
659
|
+
- Restart all applications with new credentials
|
|
660
|
+
- Verify no hardcoded credentials in code repositories
|
|
661
|
+
- Scan codebase for exposed secrets
|
|
662
|
+
|
|
663
|
+
### Monitoring for Suspicious Activity
|
|
664
|
+
|
|
665
|
+
```javascript
|
|
666
|
+
// Monitor for unusual SMS volume
|
|
667
|
+
fastify.addHook('onResponse', async (request, reply) => {
|
|
668
|
+
if (request.url.includes('/send-sms')) {
|
|
669
|
+
const metric = {
|
|
670
|
+
timestamp: Date.now(),
|
|
671
|
+
status: reply.statusCode,
|
|
672
|
+
endpoint: request.url,
|
|
673
|
+
ip: request.ip
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
await fastify.redis.lpush('sms:metrics', JSON.stringify(metric));
|
|
677
|
+
|
|
678
|
+
// Alert on 50+ SMS in 1 minute
|
|
679
|
+
const count = await fastify.redis.lrange('sms:metrics', 0, -1);
|
|
680
|
+
const recentCount = count.filter(m => {
|
|
681
|
+
const data = JSON.parse(m);
|
|
682
|
+
return Date.now() - data.timestamp < 60000;
|
|
683
|
+
}).length;
|
|
684
|
+
|
|
685
|
+
if (recentCount > 50) {
|
|
686
|
+
fastify.log.error('ALERT: High SMS volume detected', { count: recentCount });
|
|
687
|
+
// Send alert to security team
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
### Security Checklist
|
|
694
|
+
|
|
695
|
+
- [ ] All credentials stored in environment variables (not in code)
|
|
696
|
+
- [ ] `.env` file added to `.gitignore`
|
|
697
|
+
- [ ] Webhook signatures validated
|
|
698
|
+
- [ ] HTTPS enforced for all endpoints
|
|
699
|
+
- [ ] Input validation implemented
|
|
700
|
+
- [ ] Error handling sanitized (no credential exposure)
|
|
701
|
+
- [ ] Rate limiting configured
|
|
702
|
+
- [ ] Sensitive data masked in logs
|
|
703
|
+
- [ ] Audit logging implemented
|
|
704
|
+
- [ ] Incident response plan documented
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
## Additional Resources
|
|
709
|
+
|
|
710
|
+
- [Twilio Security Best Practices](https://www.twilio.com/docs/security)
|
|
711
|
+
- [SendGrid Security Documentation](https://docs.sendgrid.com/for-developers/security)
|
|
712
|
+
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
|
713
|
+
- [GDPR Compliance Guide](https://gdpr-info.eu/)
|
|
714
|
+
- [TCPA SMS Compliance](https://www.fcc.gov/consumers/guides-and-resources-related-sms-texts)
|
|
715
|
+
- [CAN-SPAM Compliance](https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business)
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
## Reporting Security Issues
|
|
720
|
+
|
|
721
|
+
If you discover a security vulnerability in xTwilio, please email security@xenterprises.com instead of using the issue tracker. Please do not disclose the vulnerability publicly until it has been addressed.
|