Saltar al contenido principal

Security

Build plugins that protect your users' data.

SSP-specific rules​

These are enforced by the platform or checked during review. Get them right first; the rest of this page is general good practice.

Credentials​

  • pik_ keys are per installation. A multi-tenant plugin must store them keyed by installation_id, encrypted at rest — never in a single shared environment variable.
  • Every secret is shown exactly once. API keys at provisioning or setup exchange; webhook secrets at provisioning, plugin creation, or rotation. Capture them straight into your secret store.
  • Rotation has no overlap window. Regenerating a key or rotating a secret invalidates the old one immediately.
  • Never expose credentials to the browser. Exchange setup tokens server-side; the operator must never see a pik_ key.
  • Revoking a developer key also suspends any sandbox operator sessions it owns — that's your containment lever if one leaks.

Webhook endpoints​

  • Public HTTPS only. SSP refuses to deliver to private, loopback, link-local, or reserved addresses, and does not follow redirects.
  • Verify X-SSP-Signature over the raw bytes, using a constant-time comparison. A body parser that consumes the stream first will break this.
  • Answer within 10 seconds. Verify, enqueue, return 2xx. Process asynchronously.
  • Be idempotent. Three delivery attempts (60s / 300s / 900s backoff) mean duplicates are normal, not exceptional.
  • Return 2xx for events you ignore. A non-2xx triggers retries that can never succeed.

Data handling​

  • Honour installation scope. A location-pinned installation must not be used to infer or store data beyond its location.
  • Don't warehouse more than you need. Customer names, emails, phone numbers and delivery addresses are personal data belonging to the restaurant, not to you.
  • Never log secrets or full payloads containing them. Log identifiers (installation_id, order_id), not bodies.

Idempotency keys are security-relevant​

external_order_id and provider_transaction_id are what stop a retried delivery-platform callback from creating a duplicate order or double-recording a payment. Always send them.


Overview​

Security is paramount when building integrations that handle sensitive data like payment information, customer details, and business metrics.


API Key Security​

Generation​

Generate cryptographically secure API keys:

// Node.js
const crypto = require('crypto');
const apiKey = crypto.randomBytes(32).toString('base64');
# Python
import secrets
api_key = secrets.token_urlsafe(32)
// PHP
$apiKey = bin2hex(random_bytes(32));

Storage​

Never hardcode API keys:

❌ Bad:

const SSP_API_KEY = 'abc123xyz'; // DON'T DO THIS

✅ Good:

const SSP_API_KEY = process.env.SSP_API_KEY;

Environment Variables​

Store all secrets in environment variables:

# .env (never commit to git!)
SSP_API_KEY=your-secure-key-here
PROVIDER_API_KEY=provider-key
WEBHOOK_SECRET=webhook-secret
DATABASE_URL=postgresql://...

Add to .gitignore:

.env
.env.local
.env.*.local

Rotation​

Rotate API keys regularly:

// Support multiple keys during rotation
const validKeys = new Set([
process.env.SSP_API_KEY_CURRENT,
process.env.SSP_API_KEY_PREVIOUS // Keep for 24h
]);

function authenticate(req, res, next) {
const apiKey = req.headers['x-api-key'];

if (!validKeys.has(apiKey)) {
return res.status(401).json({ error: 'Unauthorized' });
}

next();
}

HTTPS/TLS​

Enforce HTTPS​

Always use HTTPS in production:

// Redirect HTTP to HTTPS
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https' &&
process.env.NODE_ENV === 'production') {
return res.redirect(`https://${req.header('host')}${req.url}`);
}
next();
});

Certificate Validation​

Never disable certificate validation:

❌ Bad:

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // NEVER DO THIS

✅ Good:

const https = require('https');
const agent = new https.Agent({
rejectUnauthorized: true // Verify certificates
});

Input Validation​

Validate All Inputs​

const Joi = require('joi');

const chargeSchema = Joi.object({
amount: Joi.number().positive().max(1000000).required(),
currency: Joi.string().length(3).uppercase().required(),
customer: Joi.object({
email: Joi.string().email().required(),
phone: Joi.string().pattern(/^\+?[1-9]\d{1,14}$/)
}).required()
});

app.post('/charge', authenticate, async (req, res) => {
const { error, value } = chargeSchema.validate(req.body);

if (error) {
return res.status(400).json({
error: true,
message: 'Validation failed',
details: error.details
});
}

// Process validated data
await processCharge(value);
});

Sanitize Data​

const validator = require('validator');

function sanitizeInput(input) {
if (typeof input === 'string') {
// Trim whitespace
input = input.trim();

// Escape HTML
input = validator.escape(input);

// Remove null bytes
input = input.replace(/\0/g, '');
}

return input;
}

Prevent SQL Injection​

Use parameterized queries:

❌ Bad:

const query = `SELECT * FROM users WHERE email = '${email}'`; // SQL Injection!

✅ Good:

const query = 'SELECT * FROM users WHERE email = $1';
const result = await db.query(query, [email]);

Authentication​

Timing-Safe Comparison​

Prevent timing attacks:

const crypto = require('crypto');

function authenticate(req, res, next) {
const apiKey = req.headers['x-api-key'];
const expectedKey = process.env.SSP_API_KEY;

if (!apiKey || !expectedKey) {
return res.status(401).json({ error: 'Unauthorized' });
}

// Timing-safe comparison
const isValid = crypto.timingSafeEqual(
Buffer.from(apiKey),
Buffer.from(expectedKey)
);

if (!isValid) {
return res.status(401).json({ error: 'Unauthorized' });
}

next();
}

Rate Limiting​

Prevent brute force attacks:

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

const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: {
error: true,
message: 'Too many requests, please try again later'
},
standardHeaders: true,
legacyHeaders: false
});

app.use(limiter);

// Stricter limits for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Only 5 attempts
skipSuccessfulRequests: true
});

app.post('/auth/login', authLimiter, loginHandler);

Data Encryption​

Encrypt Sensitive Data​

const crypto = require('crypto');

class Encryption {
constructor(key) {
this.algorithm = 'aes-256-gcm';
this.key = Buffer.from(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();

return {
iv: iv.toString('hex'),
encryptedData: encrypted,
authTag: authTag.toString('hex')
};
}

decrypt(encrypted) {
const decipher = crypto.createDecipheriv(
this.algorithm,
this.key,
Buffer.from(encrypted.iv, 'hex')
);

decipher.setAuthTag(Buffer.from(encrypted.authTag, 'hex'));

let decrypted = decipher.update(encrypted.encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');

return decrypted;
}
}

// Usage
const encryption = new Encryption(process.env.ENCRYPTION_KEY);

// Store encrypted
const credentials = { apiKey: 'secret', apiSecret: 'secret' };
const encrypted = encryption.encrypt(JSON.stringify(credentials));
await db.save(encrypted);

// Retrieve and decrypt
const stored = await db.find();
const decrypted = encryption.decrypt(stored);
const credentials = JSON.parse(decrypted);

Hash Passwords​

Never store passwords in plain text:

const bcrypt = require('bcrypt');

// Hash password
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);

// Verify password
const isValid = await bcrypt.compare(password, hashedPassword);

Webhook Security​

Verify SSP signatures​

SSP signs the raw request body with your webhook_secret and sends the hex digest in X-SSP-Signature. There is no timestamp component — replay protection comes from your own idempotency handling.

const crypto = require('crypto');

function verifySSP(rawBody, receivedHex, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(receivedHex || '', 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on a length mismatch — check length first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// express.raw() is required: the HMAC covers the exact bytes SSP transmitted.
app.post('/webhooks/ssp', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifySSP(req.body, req.headers['x-ssp-signature'], process.env.SSP_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
res.json({ status: 'ok' });
});

Signing your own requests to SSP uses the same secret and the same scheme — see Authentication.

The two classic mistakes
  1. Hashing JSON.stringify(req.body) instead of the raw bytes — re-serializing changes key order and spacing.
  2. Comparing with == instead of a constant-time function.

Guard against replay​

SSP retries each delivery up to three times, so the same event legitimately arrives more than once. Deduplicate on a natural key and persist the marker:

const key = `${event.event}:${event.installation_id}:${event.data.order_id}`;
if (!await db.processedEvents.insertIfAbsent({ key })) return; // already handled

Keep the marker in your database, not in memory — a restart must not replay.


Error Handling​

Don't Leak Information​

❌ Bad:

catch (error) {
res.status(500).json({
error: error.stack, // Leaks implementation details!
query: sqlQuery,
credentials: dbCredentials
});
}

✅ Good:

catch (error) {
// Log full error server-side
console.error('Error processing request:', {
error: error.message,
stack: error.stack,
requestId: req.id
});

// Return generic error to client
res.status(500).json({
error: true,
message: 'Internal server error',
request_id: req.id // For support lookup
});
}

Structured Logging​

const winston = require('winston');

const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});

// Don't log sensitive data
logger.info('Payment processed', {
transaction_id: 'txn_123',
amount: 100.00,
// DON'T: card_number, cvv, etc.
});

Dependency Security​

Audit Dependencies​

# Node.js
npm audit
npm audit fix

# Python
pip-audit

# PHP
composer audit

Keep Dependencies Updated​

# Check for updates
npm outdated

# Update
npm update

# Major updates
npx npm-check-updates -u
npm install

Use Lock Files​

Commit lock files to ensure consistent dependencies:

  • package-lock.json (Node.js)
  • Pipfile.lock (Python)
  • composer.lock (PHP)
  • go.sum (Go)

CORS Configuration​

Configure CORS properly:

const cors = require('cors');

// Don't allow all origins in production
❌ app.use(cors()); // Allows all origins!

// Whitelist specific origins
✅ app.use(cors({
origin: [
'https://api.ssppos.com'
],
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'X-SSP-Signature', 'X-SSP-Event'],
credentials: true,
maxAge: 86400
}));

SQL Injection Prevention​

Use ORMs or Parameterized Queries​

// Sequelize ORM
const user = await User.findOne({
where: { email: email } // Safe
});

// Parameterized query
const result = await db.query(
'SELECT * FROM users WHERE email = $1',
[email] // Safe
);

// NEVER do this:
const query = `SELECT * FROM users WHERE email = '${email}'`; // DANGEROUS!

XSS Prevention​

Sanitize Output​

const validator = require('validator');

function sanitizeHTML(html) {
return validator.escape(html);
}

// In templates
<div>{{ sanitizeHTML(userInput) }}</div>

Content Security Policy​

const helmet = require('helmet');

app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'https://api.ssppos.com']
}
}));

Secrets Management​

Use Secret Management Services​

// AWS Secrets Manager
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();

async function getSecret(secretName) {
const data = await secretsManager.getSecretValue({
SecretId: secretName
}).promise();

return JSON.parse(data.SecretString);
}

// Usage
const credentials = await getSecret('ssp/plugin/razorpay');

Environment-Specific Secrets​

// config.js
module.exports = {
development: {
apiKey: process.env.DEV_API_KEY
},
production: {
apiKey: process.env.PROD_API_KEY
}
};

const config = require('./config')[process.env.NODE_ENV];

Security Headers​

Use security headers:

const helmet = require('helmet');

app.use(helmet()); // Sets multiple security headers

// Or individually:
app.use(helmet.hidePoweredBy()); // Hide X-Powered-By
app.use(helmet.hsts()); // HTTP Strict Transport Security
app.use(helmet.noSniff()); // X-Content-Type-Options
app.use(helmet.xssFilter()); // X-XSS-Protection
app.use(helmet.frameguard()); // X-Frame-Options

Monitoring & Alerting​

Security Monitoring​

// Track failed auth attempts
let failedAttempts = {};

function trackFailedAuth(ip) {
failedAttempts[ip] = (failedAttempts[ip] || 0) + 1;

if (failedAttempts[ip] > 10) {
// Alert security team
alertSecurityTeam({
type: 'brute_force',
ip: ip,
attempts: failedAttempts[ip]
});

// Block IP
blockIP(ip);
}
}

Audit Logging​

function auditLog(action, user, resource, result) {
logger.info('Audit', {
timestamp: new Date().toISOString(),
action: action,
user_id: user.id,
resource_type: resource.type,
resource_id: resource.id,
result: result, // 'success' or 'failure'
ip: user.ip
});
}

// Usage
auditLog('charge', user, { type: 'payment', id: 'txn_123' }, 'success');

Security Checklist​

SSP-specific​

  • pik_ keys stored per installation, encrypted at rest
  • webhook_endpoint is a public HTTPS URL
  • X-SSP-Signature verified over the raw body, constant-time
  • Webhook handler returns 2xx within 10 seconds
  • Webhook handling is idempotent and survives a restart
  • Setup tokens exchanged server-side, never in the browser
  • Idempotency keys sent on order creation and payment capture
  • Secrets absent from logs and error reports
  • /health is cheap and has no external dependencies
  • Installation location scope respected

General​

  • All secrets in environment variables
  • HTTPS enforced
  • API key authentication implemented
  • Timing-safe comparison used
  • Rate limiting configured
  • Input validation on all endpoints
  • SQL injection prevention (parameterized queries)
  • XSS prevention (output sanitization)
  • CORS configured properly
  • Security headers set (helmet)
  • Webhook signature verification
  • Error messages don't leak info
  • Dependencies audited
  • Logging configured (no sensitive data)
  • Monitoring and alerting set up
  • Encryption for sensitive data at rest
  • Password hashing (if applicable)
  • Certificate validation enabled

Common Vulnerabilities​

1. Mass Assignment​

❌ Bad:

app.post('/users', async (req, res) => {
const user = await User.create(req.body); // User can set isAdmin!
});

✅ Good:

app.post('/users', async (req, res) => {
const { name, email } = req.body; // Only allow specific fields
const user = await User.create({ name, email });
});

2. Insecure Direct Object References​

❌ Bad:

app.get('/transactions/:id', async (req, res) => {
const txn = await Transaction.findById(req.params.id); // Any ID!
res.json(txn);
});

✅ Good:

app.get('/transactions/:id', authenticate, async (req, res) => {
const txn = await Transaction.findOne({
id: req.params.id,
organization_id: req.user.organization_id // Check ownership
});

if (!txn) {
return res.status(404).json({ error: 'Not found' });
}

res.json(txn);
});

3. Server-Side Request Forgery (SSRF)​

❌ Bad:

app.post('/fetch-url', async (req, res) => {
const data = await axios.get(req.body.url); // Can access internal services!
res.json(data);
});

✅ Good:

const { URL } = require('url');

app.post('/fetch-url', async (req, res) => {
const url = new URL(req.body.url);

// Whitelist allowed hosts
const allowedHosts = ['api.razorpay.com', 'api.stripe.com'];
if (!allowedHosts.includes(url.hostname)) {
return res.status(400).json({ error: 'Invalid URL' });
}

const data = await axios.get(url.toString());
res.json(data);
});

Resources​


Next Steps​