8

How can I validate that a PayPal IPN POST request to my specified notifyURL is indeed coming from PayPal?

I don't mean comparing the data to what I sent earlier, but how can I verify that the server / IP address this PayPal request is coming from is indeed a valid one?

siliconpi
  • 8,105
  • 18
  • 69
  • 107
  • The methods listed here for validating where the IPN post back came from aren't foolproof, and don't really make you much more secure than you already are. Implement the best practices as recommended by PayPal. If they were insecure, PayPal would have bigger issues, considering their entire brand is built on users' trust. – Brad Feb 14 '11 at 02:19

6 Answers6

16

The IPN protocol consists of three steps:

  1. PayPal sends your IPN listener a message that notifies you of the event
  2. Your listener sends the complete unaltered message back to PayPal; the message must contain the same fields in the same order and be encoded in the same way as the original message
  3. PayPal sends a single word back, which is either VERIFIED if the message originated with PayPal or INVALID if there is any discrepancy with what was originally sent

https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_admin_IPNIntro

Amber
  • 507,862
  • 82
  • 626
  • 550
  • thanks, i'm aware of that and i'm doing that exactly... but how can I be doubly sure that its PayPal that calls my IPN listener? (the step 1 that you've copy-pasted)? – siliconpi Jan 31 '11 at 07:11
  • Is there a reason you need to be? After all, the entire point of the 3-step process outlined above is that it's impossible for someone else to call your IPN listener and still get a VERIFIED response... – Amber Jan 31 '11 at 07:12
  • I'm not 100% familiar with man-in-the-middle type attacks, but I'm thinking a stronger verification that the request is coming from PayPal would be more prudent... – siliconpi Jan 31 '11 at 10:54
  • More or less on the same topic: http://stackoverflow.com/questions/2856530/paypal-ipn-security – Sami Koivu Jan 31 '11 at 11:22
  • matt74tm: There is nothing "stronger", as long as your code is actually hard-coded to send the request to *paypal*, and not simply whatever host sent you the original request. You have two options for specifying where to send it: domain name or IP. The former could (theoretically) be compromised by someone taking over your DNS server, though that's unlikely. The latter couldn't be compromised via DNS, but could simply break if PayPal ever changed their IP. – Amber Jan 31 '11 at 18:49
  • Yes there is a good reason for this, when using multiple payment methods I would like to know which call is being made by who to then verify. – Phil Jackson Sep 18 '12 at 08:28
  • @siliconpi I agree. It's idiotic they way they have it. The IPN should be delivered over HTTPS with a client certificate. – user207421 Aug 12 '15 at 00:17
  • This is not correct: you send back the exact same message to PayPal, EXCEPT with "&cmd=_notify-validate" added to the end. – IrishChieftain Jan 27 '19 at 22:48
12

This is the easiest way I have found to do it, also as per PayPal suggests. I uses http_build_query() to construct the url from the post that was sent to the site from paypal. Paypal docs states that you should send this back for verification and that is what we do with file_get_contents. you will note that I use strstr to check if the word 'VERIFIED' is present and so we continue in the function, if not we return false...

$verify_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_notify-validate&' . http_build_query( $_POST );   

if( !strstr( file_get_contents( $verify_url ), 'VERIFIED' ) ) return false;
Lobos
  • 559
  • 6
  • 5
3

https://gist.github.com/mrded/a596b0d005e84bc27bad

function paypal_is_transaction_valid($data) {
  $context = stream_context_create(array(
    'http' => array(
      'header'  => "Content-type: application/x-www-form-urlencoded\r\n",
      'method'  => 'POST',
      'content' => http_build_query($data),
    ),
  ));
  $content = file_get_contents('https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_notify-validate', false, $context);

  return (bool) strstr($content, 'VERIFIED');
}
Samvel Aleqsanyan
  • 2,812
  • 4
  • 20
  • 28
mrded
  • 4,674
  • 2
  • 34
  • 36
3

HTTP header User-Agent required now!

$vrf = file_get_contents('https://www.paypal.com/cgi-bin/webscr?cmd=_notify-validate', false, stream_context_create(array(
    'http' => array(
        'header'  => "Content-type: application/x-www-form-urlencoded\r\nUser-Agent: MyAPP 1.0\r\n",
        'method'  => 'POST',
        'content' => http_build_query($_POST)
    )
)));

if ( $vrf == 'VERIFIED' ) {
    // Check that the payment_status is Completed
    // Check that txn_id has not been previously processed
    // Check that receiver_email is your Primary PayPal email
    // Check that payment_amount/payment_currency are correct
    // process payment
}
Sergey Shuchkin
  • 2,037
  • 19
  • 9
  • If I'm testing with the sandbox, would the URL be www.sandbox.paypal.com to authenticate with, or www.paypal.com? – Volomike Apr 29 '17 at 00:43
1

If I remember correctly, the PayPal uses a static IP for it's IPN calls.

So, checking for the correct IP should work.

alternatively, you could make use of gethostbyaddr or gethostbyname.

Jacco
  • 23,534
  • 17
  • 88
  • 105
-1

This is what I use:

if (preg_match('~^(?:.+[.])?paypal[.]com$~', gethostbyaddr($_SERVER['REMOTE_ADDR'])) > 0)
{
    // came from paypal.com (unless your server got r00ted)
}
Community
  • 1
  • 1
Alix Axel
  • 151,645
  • 95
  • 393
  • 500