r71642 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r71641‎ | r71642 | r71643 >
Date:18:39, 25 August 2010
Author:awjrichards
Status:deferred (Comments)
Tags:
Comment:
Fixed CSRF vulnerability in credit card processing form
Modified paths:
  • /trunk/extensions/DonationInterface/payflowpro_gateway/payflowpro_gateway.body.php (modified) (history)
  • /trunk/extensions/DonationInterface/payflowpro_gateway/payflowpro_gateway.i18n.php (modified) (history)
  • /trunk/extensions/DonationInterface/payflowpro_gateway/payflowpro_gateway.php (modified) (history)

Diff [purge]

Index: trunk/extensions/DonationInterface/payflowpro_gateway/payflowpro_gateway.body.php
@@ -37,7 +37,8 @@
3838 */
3939 public function execute( $par ) {
4040 global $wgRequest, $wgOut, $wgUser, $wgScriptPath, $wgPayFlowProGatewayCSSVersion;
41 -
 41+ session_cache_limiter( 'nocache' );
 42+ $this->fnPayflowEnsureSession();
4243 $this->setHeaders();
4344
4445 $wgOut->addHeadItem( 'validatescript', '<script type="text/javascript" language="javascript" src="' .
@@ -67,43 +68,10 @@
6869
6970
7071 $wgOut->addScript( Skin::makeVariablesScript( $scriptVars ) );
 72+ // establish the edit token to prevent csrf
 73+ global $wgPayflowGatewaySalt;
 74+ $token = $this->fnPayflowEditToken( $wgPayflowGatewaySalt ); //$wgUser->editToken( 'mrxc877668DwQQ' );
7175
72 - // create token if one doesn't already exist
73 - $token = $wgUser->editToken( 'mrxc877668DwQQ' );
74 -
75 - // Declare form post variables
76 - $data = array(
77 - 'amount' => '',
78 - 'email' => '',
79 - 'fname' => '',
80 - 'mname' => '',
81 - 'lname' => '',
82 - 'street' => '',
83 - 'city' => '',
84 - 'state' => '',
85 - 'zip' => '',
86 - 'country' => '',
87 - 'card_num' => '',
88 - 'expiration' => '',
89 - 'cvv' => '',
90 - 'currency' => '',
91 - 'payment_method' => '',
92 - 'order_id' => '',
93 - 'numAttempt' => '',
94 - 'referrer' => '',
95 - 'utm_source' => '',
96 - 'utm_medium' => '',
97 - 'utm_campaign' => '',
98 - 'language' => '',
99 - 'comment' => '',
100 - 'anonymous' => '',
101 - 'optout' => '',
102 - 'token' => $token,
103 - 'contribution_tracking_id' => '',
104 - 'data_hash' => '',
105 - 'action' => '',
106 - );
107 -
10876 $error[] = '';
10977
11078 // find out if amount was a radio button or textbox, set amount
@@ -120,6 +88,7 @@
12189
12290 // track the number of attempts the user has made
12391 $numAttempt = ( $wgRequest->getText( 'numAttempt' ) == '' ) ? '0' : $wgRequest->getText( 'numAttempt' );
 92+
12493 // Populate from data
12594 $data = array(
12695 'amount' => $amount,
@@ -149,6 +118,7 @@
150119 'anonymous' => $wgRequest->getText( 'comment-option' ),
151120 'optout' => $wgRequest->getText( 'email' ),
152121 'test_string' => $wgRequest->getText( 'process' ), //for showing payflow string during testing
 122+ 'token' => $token,
153123 'contribution_tracking_id' => $wgRequest->getText( 'contribution_tracking_id' ),
154124 'data_hash' => $wgRequest->getText( 'data_hash' ),
155125 'action' => $wgRequest->getText( 'action' ),
@@ -164,9 +134,9 @@
165135
166136 // Check form for errors and display
167137 // match token
168 - $success = $wgUser->matchEditToken( $token, 'mrxc877668DwQQ' );
169 -
170 - if( $success ) {
 138+ $token_check = ( $wgRequest->getText( 'token' ) ) ? $wgRequest->getText( 'token' ) : $token;
 139+ $token_match = $this->fnPayflowMatchEditToken( $token_check, $wgPayflowGatewaySalt );
 140+ if( $token_match ) {
171141 if( $data['payment_method'] == 'processed' ) {
172142 //increase the count of attempts
173143 ++$data['numAttempt'];
@@ -197,6 +167,7 @@
198168 wfRunHooks( 'PayflowGatewayReject', array( &$this, &$data ));
199169
200170 $this->fnPayflowDisplayDeclinedResults( '' );
 171+ $this->fnPayflowUnsetEditToken();
201172 }
202173
203174 // if the transaction was flagged for processing
@@ -204,6 +175,7 @@
205176 // expose a hook for external handling of trxns ready for processing
206177 wfRunHooks( 'PayflowGatewayProcess', array( &$this, &$data ));
207178 $this->fnPayflowProcessTransaction( $data, $payflow_data );
 179+ $this->fnPayflowUnsetEditToken();
208180 }
209181
210182 // expose a hook for any post processing
@@ -213,7 +185,11 @@
214186 //Display form for the first time
215187 $this->fnPayflowDisplayForm($data, $error);
216188 }
217 - }
 189+ } else {
 190+ // there's a token mismatch
 191+ $error['general']['token-mismatch'] = wfMsg( 'payflowpro_gateway-token-mismatch' );
 192+ $this->fnPayflowDisplayForm( $data, $error );
 193+ }
218194 }
219195
220196 /**
@@ -320,7 +296,20 @@
321297 Xml::openElement( 'div', array( 'id' => 'mw-creditcard-intro' ) ) .
322298 Xml::tags( 'p', array( 'class' => 'mw-creditcard-intro-msg' ), wfMsg( 'payflowpro_gateway-form-message' ) ) .
323299 Xml::closeElement( 'div' );
324 -
 300+
 301+ // provide a place at the top of the form for displaying general messages
 302+ if ( $error['general'] ) {
 303+ $form .= Xml::openElement( 'div', array( 'id' => 'mw-payflow-general-error' ));
 304+ if ( is_array( $error['general'] )) {
 305+ foreach ( $error['general'] as $error_msg ) {
 306+ $form .= Xml::tags( 'p', array( 'class' => 'creditcard-error-msg' ), $error_msg );
 307+ }
 308+ } else {
 309+ $form .= Xml::tags( 'p', array( 'class' => 'creditcard-error-msg' ), $error_msg );
 310+ }
 311+ $form .= Xml::closeElement( 'div' );
 312+ }
 313+
325314 // open form
326315 $form .= Xml::openElement( 'div', array( 'id' => 'mw-creditcard-form' ) ) .
327316 Xml::element( 'p', array( 'class' => 'creditcard-error-msg' ), $error['retryMsg'] ) .
@@ -962,5 +951,79 @@
963952 return $payflowCurrencies;
964953 }
965954
966 -
 955+ /**
 956+ * Establish an 'edit' token to help prevent CSRF, etc
 957+ *
 958+ * We use this in place of $wgUser->editToken() b/c currently
 959+ * $wgUser->editToken() is broken (apparently by design) for
 960+ * anonymous users. Using $wgUser->editToken() currently exposes
 961+ * a security risk for non-authenticated users. Until this is
 962+ * resolved in $wgUser, we'll use our own methods for token
 963+ * handling.
 964+ *
 965+ * @var mixed $salt
 966+ * @return string
 967+ */
 968+ function fnPayflowEditToken( $salt='' ) {
 969+ if ( !isset( $_SESSION[ 'payflowEditToken' ] )) {
 970+ //generate unsalted token to place in the session
 971+ $token = self::fnPayflowGenerateToken();
 972+ $_SESSION[ 'payflowEditToken' ] = $token;
 973+ } else {
 974+ $token = $_SESSION[ 'payflowEditToken' ];
 975+ }
 976+
 977+ if ( is_array( $salt )) {
 978+ $salt = implode( "|", $salt );
 979+ }
 980+ return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
 981+ }
 982+
 983+ /**
 984+ * Generate a token string
 985+ *
 986+ * @var mixed $salt
 987+ * @return string
 988+ */
 989+ public static function fnPayflowGenerateToken( $salt='' ) {
 990+ $token = dechex( mt_rand() ) . dechex( mt_rand() );
 991+ return md5( $token . $salt );
 992+ }
 993+
 994+ /**
 995+ * Determine the validity of a token
 996+ *
 997+ * @var string $val
 998+ * @var mixed $salt
 999+ * @return bool
 1000+ */
 1001+ function fnPayflowMatchEditToken( $val, $salt='' ) {
 1002+ // fetch a salted version of the session token
 1003+ $sessionToken = $this->fnPayflowEditToken( $salt );
 1004+ if ( $val != $session_token ) {
 1005+ wfDebug( "PayflowproGateway::fnPayflowMatchEditToken: broken session data\n" );
 1006+ }
 1007+ return $val == $sessionToken;
 1008+ }
 1009+
 1010+ /**
 1011+ * Unset the payflow edit token from a user's session
 1012+ */
 1013+ function fnPayflowUnsetEditToken() {
 1014+ unset( $_SESSION[ 'payflowEditToken' ] );
 1015+ }
 1016+
 1017+ /**
 1018+ * Ensure that we have a session set for the current user
 1019+ *
 1020+ * If we do not have a session set for the current user,
 1021+ * start the session.
 1022+ */
 1023+ public function fnPayflowEnsureSession() {
 1024+ // if the session is already started, do nothing
 1025+ if ( session_id() ) return;
 1026+
 1027+ // otherwise, fire it up using global mw function wfSetupSession
 1028+ wfSetupSession();
 1029+ }
9671030 } // end class
Index: trunk/extensions/DonationInterface/payflowpro_gateway/payflowpro_gateway.i18n.php
@@ -102,6 +102,7 @@
103103 'donate_interface-AUD' => 'AUD: Australian Dollar',
104104 'donate_interface-CAD' => 'CAD: Canadian Dollar',
105105 'donate_interface-JPY' => 'JPY: Japanese Yen',
 106+ 'payflowpro_gateway-token-mismatch' => 'Your session has expired. Please try filling out and submitting the form again.',
106107 );
107108
108109 /** Message documentation (Message documentation)
Index: trunk/extensions/DonationInterface/payflowpro_gateway/payflowpro_gateway.php
@@ -38,11 +38,20 @@
3939 $wgPayflowProUserID = ''; //if one or more users are set up, authorized user ID, else same as VENDOR
4040 $wgPayflowProPassword = ''; //merchant login password
4141
 42+/**
 43+ * A string or array of strings for making tokens more secure
 44+ *
 45+ * Please set this! If you do not, tokens are easy to get around, which can
 46+ * potentially leave you and your users vulnerable to CSRF or other forms of
 47+ * attack.
 48+ */
 49+$wgPayflowGatewaySalt = '';
 50+
4251 global $wgPayflowGatewayDBserver, $wgPayflowGatewayDBname, $wgPayflowGatewayDBuser, $wgPayflowGatewayDBpassword;
43 -$wgPayflowGatewayDBserver = ( !$wgPayflowGatewayDBserver ) ? $wgDBserver : $wgPayflowGatewayDBserver;
44 -$wgPayflowGatewayDBname = ( !$wgPayflowGatewayDBname ) ? $wgDBname : $wgPayflowGatewayDBname;
45 -$wgPayflowGatewayDBuser = ( !$wgPayflowGatewayDBuser ) ? $wgDBuser : $wgPayflowGatewayDBuser;
46 -$wgPayflowGatewayDBpassword = ( !$wgPayflowGatewayDBpassword ) ? $wgDBpassword : $wgPayflowGatewayDBpassword;
 52+$wgPayflowGatewayDBserver = $wgDBserver;
 53+$wgPayflowGatewayDBname = $wgDBname;
 54+$wgPayflowGatewayDBuser = $wgDBuser;
 55+$wgPayflowGatewayDBpassword = $wgDBpassword;
4756
4857 function payflowGatewayConnection() {
4958 global $wgPayflowGatewayDBserver, $wgPayflowGatewayDBname;

Comments

#Comment by Platonides (talk | contribs)   18:52, 25 August 2010

Good point in finding this. Although instead of replicating the functions, what about adding a parameter to User::editToken() to generate a full token even if it's an anon?

Instead of defaulting $wgPayflowGatewaySalt to , I would use $wgSecretKey or a variant of it.

#Comment by Awjrichards (talk | contribs)   00:32, 26 August 2010

Platonides, I updated this to default to $wgSecretKey as of r71680.

As per updating User::editToken(), I thought about it but at the moment need this to work with an older version of MW. Perhaps in the next few days I'll take a stab at updating User::editToken().

#Comment by Catrope (talk | contribs)   00:34, 26 August 2010

I don't think we actually *want* User::editToken() to return different things for different anons.

#Comment by Awjrichards (talk | contribs)   00:40, 26 August 2010

From what little I know about the history of how User::editToken() handles anons, what's currently in place is a hack specific to EditPage. I can understand there are times when you may not want the token to work as expected for anons and times when you do. For the time being, I'm happy keeping anon token tracking for the Payflow Gateway extension in the extension itself, but it seems like a potential security problem/gotcha to leave MW token handling the way it currently is.

#Comment by Platonides (talk | contribs)   10:52, 26 August 2010

That's why I mentioned adding a parameter to User::editToken().

Although I don't see the issue with returning different tokens to different anons. We need to set them a session anyway.

Status & tagging log