Insufficient Verification of Data Authenticity
Description
Insufficient Verification of Data Authenticity occurs when software does not sufficiently verify the origin or authenticity of data before using it in a security-critical decision. This includes accepting data without verifying signatures, trusting data from unauthenticated sources, or failing to validate that data comes from an expected sender. Attackers can spoof data sources, forge messages, or replay previously captured data to manipulate application behavior.
Risk
Without authenticity verification, attackers can impersonate trusted sources, forge messages, or tamper with data in transit. This vulnerability enables man-in-the-middle attacks, replay attacks, and data injection. Systems that trust data based solely on network location or transport encryption are vulnerable—encryption ensures confidentiality but not authenticity without additional measures. API endpoints that don't verify request signatures can be exploited to perform unauthorized actions.
Solution
Implement cryptographic verification for all security-critical data. Use digital signatures (HMAC, RSA, ECDSA) to verify data origin and integrity. Include timestamps or nonces to prevent replay attacks. Verify certificate chains for TLS connections. Implement mutual TLS (mTLS) for service-to-service communication. Use authenticated encryption (AES-GCM) rather than encryption-only modes. Validate webhook signatures from third-party services. Implement proper origin verification for cross-origin requests.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Data Tampering Attackers can modify data without detection, corrupting system state. |
| Authentication | Scope: Spoofing Unauthenticated data allows attackers to impersonate legitimate sources. |
| Access Control | Scope: Unauthorized Actions Forged requests can trigger privileged operations. |
Example Code + Solution Code
Vulnerable Code
# VULNERABLE: Accepting webhook without signature verification
from flask import Flask, request
@app.route('/webhook/payment', methods=['POST'])
def payment_webhook_vulnerable():
data = request.json
# No signature verification - anyone can send fake webhooks!
payment_id = data['payment_id']
status = data['status']
if status == 'completed':
mark_order_paid(payment_id) # Attacker can mark any order as paid!
return '', 200
# VULNERABLE: API without request signing
@app.route('/api/transfer', methods=['POST'])
def transfer_vulnerable():
# No authentication of request origin
data = request.json
# Attacker can send requests pretending to be legitimate
transfer_funds(data['from'], data['to'], data['amount'])
return jsonify({'status': 'success'})
# VULNERABLE: Trusting client-provided data
@app.route('/api/user/role')
def get_role_vulnerable():
# Role from client - not verified!
role = request.headers.get('X-User-Role')
if role == 'admin':
return get_admin_data()
return get_user_data()
// VULNERABLE: Java webhook without verification
@RestController
public class VulnerableWebhook {
@PostMapping("/webhook/payment")
public ResponseEntity<?> handlePayment(@RequestBody PaymentEvent event) {
// No signature verification!
if ("completed".equals(event.getStatus())) {
orderService.markPaid(event.getOrderId());
}
return ResponseEntity.ok().build();
}
// VULNERABLE: Deserializing without authenticity check
@PostMapping("/api/message")
public ResponseEntity<?> processMessage(@RequestBody byte[] data) {
// No verification that data is from trusted source!
Message message = deserialize(data);
processMessage(message);
return ResponseEntity.ok().build();
}
}
// VULNERABLE: Node.js webhook without signature
app.post('/webhook/github', (req, res) => {
const event = req.body;
// No signature verification - anyone can trigger!
if (event.action === 'push') {
triggerDeployment(event.repository);
}
res.sendStatus(200);
});
// VULNERABLE: Accepting tokens without verification
app.get('/api/data', (req, res) => {
const token = req.headers.authorization;
// Only checking if token exists, not verifying it!
if (token) {
res.json(getSensitiveData());
} else {
res.status(401).send('Unauthorized');
}
});
Fixed Code
# SAFE: Webhook with signature verification
import hmac
import hashlib
from flask import Flask, request, abort
WEBHOOK_SECRET = os.environ['WEBHOOK_SECRET']
def verify_signature(payload, signature, secret):
"""Verify HMAC signature."""
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
@app.route('/webhook/payment', methods=['POST'])
def payment_webhook_safe():
# Get signature from header
signature = request.headers.get('X-Signature')
if not signature:
abort(401, 'Missing signature')
# Verify signature
if not verify_signature(request.data, signature, WEBHOOK_SECRET):
abort(401, 'Invalid signature')
# Now safe to process
data = request.json
payment_id = data['payment_id']
status = data['status']
if status == 'completed':
mark_order_paid(payment_id)
return '', 200
# SAFE: API with request signing
import time
from functools import wraps
def verify_request_signature(f):
@wraps(f)
def decorated(*args, **kwargs):
timestamp = request.headers.get('X-Timestamp')
signature = request.headers.get('X-Signature')
api_key = request.headers.get('X-API-Key')
if not all([timestamp, signature, api_key]):
abort(401, 'Missing authentication headers')
# Check timestamp to prevent replay attacks
request_time = int(timestamp)
current_time = int(time.time())
if abs(current_time - request_time) > 300: # 5 minute window
abort(401, 'Request expired')
# Get secret for API key
secret = get_api_secret(api_key)
if not secret:
abort(401, 'Invalid API key')
# Build string to sign
string_to_sign = f"{request.method}\n{request.path}\n{timestamp}\n"
if request.data:
string_to_sign += hashlib.sha256(request.data).hexdigest()
# Verify signature
expected = hmac.new(
secret.encode(),
string_to_sign.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401, 'Invalid signature')
return f(*args, **kwargs)
return decorated
@app.route('/api/transfer', methods=['POST'])
@verify_request_signature
def transfer_safe():
data = request.json
transfer_funds(data['from'], data['to'], data['amount'])
return jsonify({'status': 'success'})
# SAFE: GitHub webhook verification
import hashlib
import hmac
GITHUB_WEBHOOK_SECRET = os.environ['GITHUB_WEBHOOK_SECRET']
@app.route('/webhook/github', methods=['POST'])
def github_webhook_safe():
signature = request.headers.get('X-Hub-Signature-256')
if not signature:
abort(401, 'Missing signature')
# Verify GitHub signature
expected = 'sha256=' + hmac.new(
GITHUB_WEBHOOK_SECRET.encode(),
request.data,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401, 'Invalid signature')
event = request.json
event_type = request.headers.get('X-GitHub-Event')
if event_type == 'push':
trigger_deployment(event['repository'])
return '', 200
// SAFE: Java webhook with signature verification
@RestController
public class SecureWebhook {
@Value("${webhook.secret}")
private String webhookSecret;
@PostMapping("/webhook/payment")
public ResponseEntity<?> handlePayment(
@RequestBody String payload,
@RequestHeader("X-Signature") String signature) {
// Verify signature
if (!verifySignature(payload, signature)) {
return ResponseEntity.status(401).body("Invalid signature");
}
PaymentEvent event = objectMapper.readValue(payload, PaymentEvent.class);
if ("completed".equals(event.getStatus())) {
orderService.markPaid(event.getOrderId());
}
return ResponseEntity.ok().build();
}
private boolean verifySignature(String payload, String signature) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec key = new SecretKeySpec(webhookSecret.getBytes(), "HmacSHA256");
mac.init(key);
String expected = "sha256=" + bytesToHex(mac.doFinal(payload.getBytes()));
return MessageDigest.isEqual(
expected.getBytes(),
signature.getBytes()
);
} catch (Exception e) {
return false;
}
}
}
// SAFE: Request signing service
@Service
public class RequestSigningService {
public Map<String, String> signRequest(String method, String path, byte[] body, String apiKey, String secret) {
long timestamp = System.currentTimeMillis() / 1000;
String bodyHash = body != null ?
Hex.encodeHexString(MessageDigest.getInstance("SHA-256").digest(body)) : "";
String stringToSign = String.format("%s\n%s\n%d\n%s", method, path, timestamp, bodyHash);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
String signature = Hex.encodeHexString(mac.doFinal(stringToSign.getBytes()));
return Map.of(
"X-API-Key", apiKey,
"X-Timestamp", String.valueOf(timestamp),
"X-Signature", signature
);
}
public boolean verifyRequest(HttpServletRequest request, String secret) {
String timestamp = request.getHeader("X-Timestamp");
String signature = request.getHeader("X-Signature");
if (timestamp == null || signature == null) {
return false;
}
// Check timestamp
long requestTime = Long.parseLong(timestamp);
long currentTime = System.currentTimeMillis() / 1000;
if (Math.abs(currentTime - requestTime) > 300) {
return false; // Expired
}
// Rebuild and verify signature
// ... (same logic as signing)
return MessageDigest.isEqual(expected.getBytes(), signature.getBytes());
}
}
// SAFE: Node.js webhook with signature verification
const crypto = require('crypto');
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
function verifySignature(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post('/webhook/payment', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'];
if (!signature) {
return res.status(401).send('Missing signature');
}
if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
if (event.status === 'completed') {
markOrderPaid(event.payment_id);
}
res.sendStatus(200);
});
// SAFE: Stripe webhook verification
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook/stripe',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = req.headers['stripe-signature'];
try {
// Stripe library handles verification
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
// Event is verified
switch (event.type) {
case 'payment_intent.succeeded':
handlePaymentSuccess(event.data.object);
break;
}
res.json({ received: true });
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
res.status(400).send(`Webhook Error: ${err.message}`);
}
}
);
// SAFE: API request signing middleware
function signedRequestMiddleware(secret) {
return (req, res, next) => {
const timestamp = req.headers['x-timestamp'];
const signature = req.headers['x-signature'];
const apiKey = req.headers['x-api-key'];
if (!timestamp || !signature || !apiKey) {
return res.status(401).json({ error: 'Missing authentication headers' });
}
// Check timestamp
const requestTime = parseInt(timestamp, 10);
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - requestTime) > 300) {
return res.status(401).json({ error: 'Request expired' });
}
// Get secret for API key
const clientSecret = getClientSecret(apiKey);
if (!clientSecret) {
return res.status(401).json({ error: 'Invalid API key' });
}
// Build string to sign
let bodyHash = '';
if (req.body && Object.keys(req.body).length > 0) {
bodyHash = crypto
.createHash('sha256')
.update(JSON.stringify(req.body))
.digest('hex');
}
const stringToSign = `${req.method}\n${req.path}\n${timestamp}\n${bodyHash}`;
const expected = crypto
.createHmac('sha256', clientSecret)
.update(stringToSign)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
};
}
app.use('/api/', signedRequestMiddleware());
Exploited in the Wild
Webhook Spoofing Attacks
Payment providers like Stripe and PayPal have documented attacks where attackers send forged webhook notifications to mark orders as paid without actual payment.
API Replay Attacks
Systems without timestamp verification in signatures have been exploited through captured and replayed requests.
OAuth Token Theft
Applications accepting tokens without proper validation have been compromised through stolen or forged tokens.
Tools to test/exploit
-
Burp Suite — intercept and modify requests.
-
Postman — craft custom requests without signatures.
-
Custom scripts to replay captured requests.
CVE Examples
-
CVE-2021-41117 — Authenticity verification bypass.
-
CVE-2020-8945 — GPGME signature verification bypass.
-
CVE-2019-1010266 — Webhook signature bypass.
References
-
MITRE. "CWE-345: Insufficient Verification of Data Authenticity." https://cwe.mitre.org/data/definitions/345.html
-
OWASP. "Webhook Security." https://cheatsheetseries.owasp.org/cheatsheets/Webhook_Security_Cheat_Sheet.html