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

ImpactDetails
IntegrityScope: Data Tampering

Attackers can modify data without detection, corrupting system state.
AuthenticationScope: Spoofing

Unauthenticated data allows attackers to impersonate legitimate sources.
Access ControlScope: 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


References

  1. MITRE. "CWE-345: Insufficient Verification of Data Authenticity." https://cwe.mitre.org/data/definitions/345.html

  2. OWASP. "Webhook Security." https://cheatsheetseries.owasp.org/cheatsheets/Webhook_Security_Cheat_Sheet.html