Updated CFHttpSession.cfc With Spoofed Referer Can Log Into PayPal
Yesterday, Steve Stout brought it to my attention that my CFHttpSession.cfc ColdFusion component does not use any Referer spoofing. When I read that, I almost couldn't believe my eyes; I have spent a good amount of my time playing around with CFHttp and CFHttpParam and the fact that I forgot to put in referral spoofing blows my mind a little bit. So, this morning, I went back in and added the functionality. When you launch a new request with a given URL, you have the option to pass in a referer spoof as the second argument:
CFHttpSession.NewRequest( URL [, Referer ] )
But, I doubt that you will ever need it since I have also added in automatic referer usage based on previous URLs in the request flow. Everytime you make a new request, it checks to see if you have made a previous request with the same CFHttpSession.cfc instance. If you have, it uses the previous target URL and the current referer. This way, your page requests are really mimicing the action of a real browser.
In order to mimic a real browser, you have to think like a real user. How would a real user work? A real user wouldn't just jump directly to a form processing page. No, a real user would go to the form page first, then submit the form. This is the same mentality you have to use when using the CFHttpSession.cfc ColdFusion component; when you want to log into a site, you have to view the login page first, then submit the form with a subsequent request. This way, you can set up all the session cookies and live within the session rules set forth by the site for real live users.
Steve Stout also pointed out that he was getting connection issues when connecting to a HTTPS page on pb.com (which is actually how the discussion of referral spoofing came up). I don't have a login to pb.com, but I do have a login for PayPal.com which also uses HTTPS for their secure pages (as do most all secure pages). So, I ran some tests on logging into PayPal.com to make sure that the referral spoofing was working properly:
<!---
Create the CFHttpSession object that will be sued to
make our multiple calls to the same remote application.
--->
<cfset objHttpSession = CreateObject(
"component",
"CFHTTPSession"
).Init()
/>
<!---
Call the PayPal website. We need to call the homepage
first to set up the proper cookies and referer. If we
try to access the login page directly, we will get
denies access.
--->
<cfset objResponse = objHttpSession
.NewRequest( "http://www.paypal.com" )
.Get()
/>
<!---
Now that we have our session set up, let's go ahead
and log into PayPal.com.
--->
<cfset objResponse = objHttpSession
.NewRequest( "https://www.paypal.com/us/cgi-bin/webscr" )
.AddUrl( "cmd", "_login-submit" )
.AddFormField( "login_email", "fergie@blackeyedpeas.com" )
.AddFormField( "login_password", "myLadyLumps" )
.AddFormField( "submit.x", "Log In" )
.AddFormField( "form_charset", "UTF-8" )
.Post()
/>
<!---
At this point, we should be logged into the system. Now,
let's hop over to the account overview page.
--->
<cfset objResponse = objHttpSession
.NewRequest( "https://www.paypal.com/us/cgi-bin/webscr" )
.AddUrl( "cmd", "_account" )
.AddUrl( "nav", "0" )
.Get()
/>
<!---
To make sure that everything is working, output the
PayPal account overview page.
--->
<cfoutput>
<h1>
From My Server:
</h1>
<br />
<div style="width: 500px ; height: 400 ; border: 4px solid gold ; overflow: auto ;">
#objResponse.FileContent#
</div>
</cfoutput>
Notice that we are taking three steps to get to the Account Overview page:
- Go to homepage (login form)
- Submit login credentials
- Go to account overview page
Using this page flow, we mimic a real user and build a proper sessoin. And, running the code above, we get the following output:
As you can see, using ColdFusion and CFHttpSession.cfc, I was able to successfully log into my PayPal.com account and then further access my Account Overview page.
Here is the code for the updated CFHttpSessoin.cfc with referral spoofing (and a few other updates here and there):
<cfcomponent
output="false"
hint="Handles a CFHTTP session by sending an receving cookies behind the scenes.">
<!---
Pseudo constructor. Set up data structures and
default values.
--->
<cfset VARIABLES.Instance = {} />
<!---
These are the cookies that get returned from the
request that enable us to keep the session across
different CFHttp requests.
--->
<cfset VARIABLES.Instance.Cookies = {} />
<!---
The request data contains the various types of data that
we will send with our request. These will be both for the
CFHttpParam tags as well as the CFHttp property values.
--->
<cfset VARIABLES.Instance.RequestData = {} />
<cfset VARIABLES.Instance.RequestData.Url = "" />
<cfset VARIABLES.Instance.RequestData.Referer = "" />
<cfset VARIABLES.Instance.RequestData.UserAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6" />
<cfset VARIABLES.Instance.RequestData.Params = [] />
<cffunction
name="Init"
access="public"
returntype="any"
output="false"
hint="Returns an initialized component.">
<!--- Define arguments. --->
<cfargument
name="UserAgent"
type="string"
required="false"
hint="The user agent that will be used on the subseqent page requests."
/>
<!--- Check to see if we have a user agent. --->
<cfif StructKeyExists( ARGUMENTS, "UserAgent" )>
<cfset THIS.SetUserAgent( ARGUMENTS.UserAgent ) />
</cfif>
<!--- Return This reference. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="AddCGI"
access="public"
returntype="any"
output="false"
hint="Adds a CGI value. Returns THIS scope for method chaining.">
<!--- Define arguments. --->
<cfargument
name="Name"
type="string"
required="true"
hint="The name of the CGI value."
/>
<cfargument
name="Value"
type="string"
required="true"
hint="The CGI value."
/>
<cfargument
name="Encoded"
type="string"
required="false"
default="yes"
hint="Determins whether or not to encode the CGI value."
/>
<!--- Set parameter and return This reference. --->
<cfreturn THIS.AddParam(
Type = "CGI",
Name = ARGUMENTS.Name,
Value = ARGUMENTS.Value,
Encoded = ARGUMENTS.Encoded
) />
</cffunction>
<cffunction
name="AddCookie"
access="public"
returntype="any"
output="false"
hint="Adds a cookie value. Returns THIS scope for method chaining.">
<!--- Define arguments. --->
<cfargument
name="Name"
type="string"
required="true"
hint="The name of the CGI value."
/>
<cfargument
name="Value"
type="string"
required="true"
hint="The CGI value."
/>
<!--- Set parameter and return This reference. --->
<cfreturn THIS.AddParam(
Type = "Cookie",
Name = ARGUMENTS.Name,
Value = ARGUMENTS.Value
) />
</cffunction>
<cffunction
name="AddFile"
access="public"
returntype="any"
output="false"
hint="Adds a file value. Returns THIS scope for method chaining.">
<!--- Define arguments. --->
<cfargument
name="Name"
type="string"
required="true"
hint="The name of the form field for the posted file."
/>
<cfargument
name="Path"
type="string"
required="true"
hint="The expanded path to the file."
/>
<cfargument
name="MimeType"
type="string"
required="false"
default="application/octet-stream"
hint="The mime type of the posted file. Defaults to *unknown* mime type."
/>
<!--- Set parameter and return This reference. --->
<cfreturn THIS.AddParam(
Type = "Cookie",
Name = ARGUMENTS.Name,
Value = ARGUMENTS.Value
) />
</cffunction>
<cffunction
name="AddFormField"
access="public"
returntype="any"
output="false"
hint="Adds a form value. Returns THIS scope for method chaining.">
<!--- Define arguments. --->
<cfargument
name="Name"
type="string"
required="true"
hint="The name of the form field."
/>
<cfargument
name="Value"
type="string"
required="true"
hint="The form field value."
/>
<cfargument
name="Encoded"
type="string"
required="false"
default="yes"
hint="Determins whether or not to encode the form value."
/>
<!--- Set parameter and return This reference. --->
<cfreturn THIS.AddParam(
Type = "FormField",
Name = ARGUMENTS.Name,
Value = ARGUMENTS.Value,
Encoded = ARGUMENTS.Encoded
) />
</cffunction>
<cffunction
name="AddHeader"
access="public"
returntype="any"
output="false"
hint="Adds a header value. Returns THIS scope for method chaining.">
<!--- Define arguments. --->
<cfargument
name="Name"
type="string"
required="true"
hint="The name of the header value."
/>
<cfargument
name="Value"
type="string"
required="true"
hint="The header value."
/>
<!--- Set parameter and return This reference. --->
<cfreturn THIS.AddParam(
Type = "Header",
Name = ARGUMENTS.Name,
Value = ARGUMENTS.Value
) />
</cffunction>
<cffunction
name="AddParam"
access="public"
returntype="any"
output="false"
hint="Adds a CFHttpParam data point. Returns THIS scope for method chaining.">
<!--- Define arguments. --->
<cfargument
name="Type"
type="string"
required="true"
hint="The type of data point."
/>
<cfargument
name="Name"
type="string"
required="true"
hint="The name of the data point."
/>
<cfargument
name="Value"
type="any"
required="true"
hint="The value of the data point."
/>
<cfargument
name="File"
type="string"
required="false"
default=""
hint="The expanded path to be used if the data piont is a file."
/>
<cfargument
name="MimeType"
type="string"
required="false"
default=""
hint="The mime type of the file being passed (if file is being passed)."
/>
<cfargument
name="Encoded"
type="string"
required="false"
default="yes"
hint="The determines whether or not to encode Form Field and CGI values."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!---
Check to see which kind of data point we are dealing
with so that we can see how to create the param.
--->
<cfswitch expression="#ARGUMENTS.Type#">
<cfcase value="Body">
<!--- Create the param. --->
<cfset LOCAL.Param = {
Type = ARGUMENTS.Type,
Value = ARGUMENTS.Value
} />
</cfcase>
<cfcase value="CGI">
<!--- Create the param. --->
<cfset LOCAL.Param = {
Type = ARGUMENTS.Type,
Name = ARGUMENTS.Name,
Value = ARGUMENTS.Value,
Encoded = ARGUMENTS.Encoded
} />
</cfcase>
<cfcase value="Cookie">
<!--- Create the param. --->
<cfset LOCAL.Param = {
Type = ARGUMENTS.Type,
Name = ARGUMENTS.Name,
Value = ARGUMENTS.Value
} />
</cfcase>
<cfcase value="File">
<!--- Create the param. --->
<cfset LOCAL.Param = {
Type = ARGUMENTS.Type,
Name = ARGUMENTS.Name,
File = ARGUMENTS.File,
MimeType = ARGUMENTS.MimeType
} />
</cfcase>
<cfcase value="FormField">
<!--- Create the param. --->
<cfset LOCAL.Param = {
Type = ARGUMENTS.Type,
Name = ARGUMENTS.Name,
Value = ARGUMENTS.Value,
Encoded = ARGUMENTS.Encoded
} />
</cfcase>
<cfcase value="Header">
<!--- Create the param. --->
<cfset LOCAL.Param = {
Type = ARGUMENTS.Type,
Name = ARGUMENTS.Name,
Value = ARGUMENTS.Value
} />
</cfcase>
<cfcase value="Url">
<!--- Create the param. --->
<cfset LOCAL.Param = {
Type = ARGUMENTS.Type,
Name = ARGUMENTS.Name,
Value = ARGUMENTS.Value
} />
</cfcase>
<cfcase value="Xml">
<!--- Create the param. --->
<cfset LOCAL.Param = {
Type = ARGUMENTS.Type,
Value = ARGUMENTS.Value
} />
</cfcase>
</cfswitch>
<!--- Add the parameter for the next request. --->
<cfset ArrayAppend(
VARIABLES.Instance.RequestData.Params,
LOCAL.Param
) />
<!--- Return This reference. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="AddUrl"
access="public"
returntype="any"
output="false"
hint="Adds a url value. Returns THIS scope for method chaining.">
<!--- Define arguments. --->
<cfargument
name="Name"
type="string"
required="true"
hint="The name of the header value."
/>
<cfargument
name="Value"
type="string"
required="true"
hint="The header value."
/>
<!--- Set parameter and return This reference. --->
<cfreturn THIS.AddParam(
Type = "Url",
Name = ARGUMENTS.Name,
Value = ARGUMENTS.Value
) />
</cffunction>
<cffunction
name="Get"
access="public"
returntype="struct"
output="false"
hint="Uses the GET method to place the next request. Returns the CFHttp response.">
<!--- Define arguments. --->
<cfargument
name="GetAsBinary"
type="string"
required="false"
default="auto"
hint="Determines how to return the file content - return as binary value."
/>
<cfargument
name="Debug"
type="boolean"
required="false"
default="false"
hint="If this is true, then the response object will be dumped out and the page will be aborted."
/>
<!--- Return response. --->
<cfreturn THIS.Request(
Method = "get",
GetAsBinary = ARGUMENTS.GetAsBinary,
Debug = ARGUMENTS.Debug
) />
</cffunction>
<cffunction
name="GetCookies"
access="public"
returntype="struct"
output="false"
hint="Returns the internal session cookies.">
<cfreturn VARIABLES.Instance.Cookies />
</cffunction>
<cffunction
name="NewRequest"
access="public"
returntype="any"
output="false"
hint="Sets up the object for a new request. Returns THIS scope for method chaining.">
<!--- Define arguments. --->
<cfargument
name="Url"
type="string"
required="true"
hint="The URL for the new request."
/>
<cfargument
name="Referer"
type="string"
required="false"
default=""
hint="The referring URL for the request. By default, it will be the same directory as the target URL."
/>
<!---
Before we store the URL, let's check to see if we
already had one in memory. If so, then we can use
that for a referer (which we then have the option
to override. The point here is that each URL can
be the referer for the next one.
--->
<cfif Len( VARIABLES.Instance.RequestData.Url )>
<!---
Store the previous url as the next referer. We
may override this in a second.
--->
<cfset VARIABLES.Instance.RequestData.Referer = VARIABLES.Instance.RequestData.Url />
</cfif>
<!--- Store the passed-in url. --->
<cfset VARIABLES.Instance.RequestData.Url = ARGUMENTS.Url />
<!---
Check to see if the referer was passed in. Since we
are using previous URLs as the next referring url,
we only want to store the passed in value if it has
length
--->
<cfif Len( ARGUMENTS.Referer )>
<!--- Store manually set referer. --->
<cfset VARIABLES.Instance.RequestData.Referer = ARGUMENTS.Referer />
</cfif>
<!--- Clear the request data. --->
<cfset VARIABLES.Instance.RequestData.Params = [] />
<!--- Return This reference. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="Post"
access="public"
returntype="struct"
output="false"
hint="Uses the POST method to place the next request. Returns the CFHttp response.">
<!--- Define arguments. --->
<cfargument
name="GetAsBinary"
type="string"
required="false"
default="auto"
hint="Determines how to return the file content - return as binary value."
/>
<cfargument
name="Debug"
type="boolean"
required="false"
default="false"
hint="If this is true, then the response object will be dumped out and the page will be aborted."
/>
<!--- Return response. --->
<cfreturn THIS.Request(
Method = "post",
GetAsBinary = ARGUMENTS.GetAsBinary,
Debug = ARGUMENTS.Debug
) />
</cffunction>
<cffunction
name="Request"
access="public"
returntype="struct"
output="false"
hint="Performs the CFHttp request and returns the response.">
<!--- Define arguments. --->
<cfargument
name="Method"
type="string"
required="false"
default="get"
hint="The type of request to make."
/>
<cfargument
name="GetAsBinary"
type="string"
required="false"
default="auto"
hint="Determines how to return body."
/>
<cfargument
name="Debug"
type="boolean"
required="false"
default="false"
hint="If this is true, then the response object will be dumped out and the page will be aborted."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!---
Make request. When the request comes back, we don't
want to follow any redirects. We want this to be
done manually.
--->
<cfhttp
url="#VARIABLES.Instance.RequestData.Url#"
method="#ARGUMENTS.Method#"
useragent="#VARIABLES.Instance.RequestData.UserAgent#"
getasbinary="#ARGUMENTS.GetAsBinary#"
redirect="no"
result="LOCAL.Get">
<!---
In order to maintain the user's session, we are
going to resend any cookies that we have stored
internally.
--->
<cfloop
item="LOCAL.Key"
collection="#VARIABLES.Instance.Cookies#">
<cfhttpparam
type="cookie"
name="#LOCAL.Key#"
value="#VARIABLES.Instance.Cookies[ LOCAL.Key ].Value#"
/>
</cfloop>
<!---
At this point, we have done everything that we
need to in order to maintain the user's session
across CFHttp requests. Now we can go ahead and
pass along any addional data that has been specified.
--->
<!--- Let's spoof the referer. --->
<cfhttpparam
type="header"
name="referer"
value="#VARIABLES.Instance.RequestData.Referer#"
/>
<!--- Loop over params. --->
<cfloop
index="LOCAL.Param"
array="#VARIABLES.Instance.RequestData.Params#">
<!---
Pass the existing param object in as our
attributes collection.
--->
<cfhttpparam
attributecollection="#LOCAL.Param#"
/>
</cfloop>
</cfhttp>
<!---
Check to see if we are debugging. If so, then we
will dump out the request response and abort the
page flow.
--->
<cfif ARGUMENTS.Debug>
<!--- Dump and abort. --->
<cfdump var="#VARIABLES.Instance.RequestData#" />
<cfset WriteOutput( LOCAL.Get.FileContent ) />
<cfdump var="#LOCAL.Get#" />
<cfabort />
</cfif>
<!---
Store the response cookies into our internal cookie
storage struct.
--->
<cfset StoreResponseCookies( LOCAL.Get ) />
<!---
Check to see if there was some sort of redirect
returned with the repsonse. If there was, we want
to redirect with the proper value.
--->
<cfif StructKeyExists( LOCAL.Get.ResponseHeader, "Location" )>
<!---
There was a response, so now we want to do a
recursive call to return the next page. When
we do this, make sure we have the proper URL
going out.
--->
<cfif REFindNoCase(
"^http",
LOCAL.Get.ResponseHeader.Location
)>
<!--- Proper url. --->
<cfreturn THIS
.NewRequest( LOCAL.Get.ResponseHeader.Location )
.Get()
/>
<cfelse>
<!---
Non-root url. We need to append the current
redirect url to our last URL for relative
path traversal.
--->
<cfreturn THIS
.NewRequest(
GetDirectoryFromPath( VARIABLES.Instance.RequestData.Url ) &
LOCAL.Get.ResponseHeader.Location
)
.Get()
/>
</cfif>
<cfelse>
<!---
No redirect, so just return the current
request response object.
--->
<cfreturn LOCAL.Get />
</cfif>
</cffunction>
<cffunction
name="SetBody"
access="public"
returntype="any"
output="false"
hint="Sets the body data of next request. Returns THIS scope for method chaining.">
<!--- Define arguments. --->
<cfargument
name="Value"
type="any"
required="false"
hint="The data body."
/>
<!--- Set parameter and return This reference. --->
<cfreturn THIS.AddParam(
Type = "Body",
Name = "",
Value = ARGUMENTS.Value
) />
</cffunction>
<cffunction
name="SetUserAgent"
access="public"
returntype="any"
output="false"
hint="Sets the user agent for next request. Returns THIS scope for method chaining.">
<!--- Define arguments. --->
<cfargument
name="Value"
type="string"
required="false"
hint="The user agent that will be used on the subseqent page requests."
/>
<!--- Store value. --->
<cfset VARIABLES.Instance.RequestData.UserAgent = ARGUMENTS.UserAgent />
<!--- Return This reference. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="SetXml"
access="public"
returntype="any"
output="false"
hint="Sets the XML body data of next request. Returns THIS scope for method chaining.">
<!--- Define arguments. --->
<cfargument
name="Value"
type="any"
required="false"
hint="The data body."
/>
<!--- Set parameter and return This reference. --->
<cfreturn THIS.AddParam(
Type = "Xml",
Name = "",
Value = ARGUMENTS.Value
) />
</cffunction>
<cffunction
name="StoreResponseCookies"
access="public"
returntype="void"
output="false"
hint="This parses the response of a CFHttp call and puts the cookies into a struct.">
<!--- Define arguments. --->
<cfargument
name="Response"
type="struct"
required="true"
hint="The response of a CFHttp call."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = StructNew() />
<!---
Create the default struct in which we will hold
the response cookies. This struct will contain structs
and will be keyed on the name of the cookie to be set.
--->
<cfset LOCAL.Cookies = StructNew() />
<!---
Get a reference to the cookies that werew returned
from the page request. This will give us an numericly
indexed struct of cookie strings (which we will have
to parse out for values). BUT, check to make sure
that cookies were even sent in the response. If they
were not, then there is not work to be done.
--->
<cfif NOT StructKeyExists(
ARGUMENTS.Response.ResponseHeader,
"Set-Cookie"
)>
<!--- No cookies were send back so just return. --->
<cfreturn />
</cfif>
<!---
ASSERT: We know that cookie were returned in the page
response and that they are available at the key,
"Set-Cookie" of the reponse header.
--->
<!---
The cookies might be coming back as a struct or they
might be coming back as a string. If there is only
ONE cookie being retunred, then it comes back as a
string. If that is the case, then re-store it as a
struct.
--->
<cfif IsSimpleValue( ARGUMENTS.Response.ResponseHeader[ "Set-Cookie" ] )>
<cfset LOCAL.ReturnedCookies = {} />
<cfset LOCAL.ReturnedCookies[ 1 ] = ARGUMENTS.Response.ResponseHeader[ "Set-Cookie" ] />
<cfelse>
<!--- Get a reference to the cookies struct. --->
<cfset LOCAL.ReturnedCookies = ARGUMENTS.Response.ResponseHeader[ "Set-Cookie" ] />
</cfif>
<!---
At this point, we know that no matter how the
cookies came back, we have the cookies in a
structure of cookie values.
--->
<cfloop
item="LOCAL.CookieIndex"
collection="#LOCAL.ReturnedCookies#">
<!---
As we loop through the cookie struct, get
the cookie string we want to parse.
--->
<cfset LOCAL.CookieString = LOCAL.ReturnedCookies[ LOCAL.CookieIndex ] />
<!---
For each of these cookie strings, we are going
to need to parse out the values. We can treate
the cookie string as a semi-colon delimited list.
--->
<cfloop
index="LOCAL.Index"
from="1"
to="#ListLen( LOCAL.CookieString, ';' )#"
step="1">
<!--- Get the name-value pair. --->
<cfset LOCAL.Pair = ListGetAt(
LOCAL.CookieString,
LOCAL.Index,
";"
) />
<!---
Get the name as the first part of the pair
sepparated by the equals sign.
--->
<cfset LOCAL.Name = ListFirst( LOCAL.Pair, "=" ) />
<!---
Check to see if we have a value part. Not all
cookies are going to send values of length,
which can throw off ColdFusion.
--->
<cfif (ListLen( LOCAL.Pair, "=" ) GT 1)>
<!--- Grab the rest of the list. --->
<cfset LOCAL.Value = ListRest( LOCAL.Pair, "=" ) />
<cfelse>
<!---
Since ColdFusion did not find more than
one value in the list, just get the empty
string as the value.
--->
<cfset LOCAL.Value = "" />
</cfif>
<!---
Now that we have the name-value data values,
we have to store them in the struct. If we
are looking at the first part of the cookie
string, this is going to be the name of the
cookie and it's struct index.
--->
<cfif (LOCAL.Index EQ 1)>
<!---
Create a new struct with this cookie's name
as the key in the return cookie struct.
--->
<cfset LOCAL.Cookies[ LOCAL.Name ] = StructNew() />
<!---
Now that we have the struct in place, lets
get a reference to it so that we can refer
to it in subseqent loops.
--->
<cfset LOCAL.Cookie = LOCAL.Cookies[ LOCAL.Name ] />
<!--- Store the value of this cookie. --->
<cfset LOCAL.Cookie.Value = LOCAL.Value />
<!---
Now, this cookie might have more than just
the first name-value pair. Let's create an
additional attributes struct to hold those
values.
--->
<cfset LOCAL.Cookie.Attributes = StructNew() />
<cfelse>
<!---
For all subseqent calls, just store the
name-value pair into the established
cookie's attributes strcut.
--->
<cfset LOCAL.Cookie.Attributes[ LOCAL.Name ] = LOCAL.Value />
</cfif>
</cfloop>
</cfloop>
<!---
Now that we have all the response cookies in a
struct, let's append those cookies to our internal
response cookies.
--->
<cfset StructAppend(
VARIABLES.Instance.Cookies,
LOCAL.Cookies
) />
<!--- Return out. --->
<cfreturn />
</cffunction>
</cfcomponent>
Want to use code from this post? Check out the license.
Reader Comments
And I was scrolling, and scrolling... Jeez. Nice work, and lots of it!
@Sami,
Thanks man.
Ben,
As always, this is incredible. This is hopefully going to completely solve a problem we've had with automating deployment of our sites, which have to go through a 3rd party CMS system.
Anyway, I've run into an issue, where the redirect due to a Location caused a problem when the redirection wasn't a full redirect with http:// but instead an absolute redirect. It appends that new location onto the full request URL, which means that you get a 404.
Maybe an example is easiest.
http://apple.com/foo
302 Moved Temporarily
Location: /bar
With the way it's currently programmed, the new request goes to http://apple.com/foo/bar, and you get a 404.
I made a change to the component that seems to work on my end, but I'm sure I'm missing something. Let me know if you want a copy of the changed code.
Thanks again for this great tool!
@Toby,
The CFHttpSession.cfc component, when it finds a locational redirect is checking to see if the URL passed back starts with "http:". If it does, then is uses the given URL. If it does NOT, then it takes the redirect and appends it to fileless-path of the previous URL.
Are you using CFHttpSession.cfc for your stuff? If so, please let me know of any problems it is causing.
@Ben,
I guess that's my point it's NOT removing the path at the end of the URL. It's possible that the behavior is different on PC based systems, but on my sysem, if you send in the URL http://apple.com/foo but there's a location redirect to /bar, the component doesn't request http://apple.com/bar but instead asks for http://apple.com/foo/bar. This might be related to the GetDirectoryFromPath() function working differently on PC vs. *nix based systems (I'm using OS X).
I haven't started actively using the component yet -- I was just testing it out to see if it could fulfill a need we have. I haven't got it to work 100% yet (need to work out a few kinks on my end).
Thanks again,
Toby
Just to be clear, I'm talking about an instance where /foo and /bar are both directory paths (i.e. it could be /foo/, /bar/, but the system I connect to is just returning /bar).
I can show you the specific part of the code that's affected, if you'd like.
I missed your original blog post "Passing Referer AS ColdFusion CFHttp CGI Value vs HEADER Value" from last August. That was damned clever of you, Ben!
Ironically, a new spam comment was added to that very post a few weeks ago. At least, I think it's spam. If not, you have a seriously tweaked fan base. :)
@Toby,
That's odd. The GetDirectoryFromPath() should strip off "foo" from ..../foo since Foo does not have a trailing slash. I will look into it some more.
@Ben,
Hmmm....well, the example I gave maybe wasn't great. Let me use something closer to reality.
Let's say the URL was:
http://domain.com/do/action
And then it had a 302 redirect to /do/otheraction
Right now, your code would redirect it to /do/action/do/otheraction.
BUT even if you stripped off the first unslashed entry from the original URL, so you got /do/do/otheraction, you'd still get a 404.
My guess is that you have to strip off the whole path. Does that not match the specification for HTTP 302 redirects?
Thanks,
Toby
@Toby,
Ahhh, thanks. I see what the problem is. I can't just check for http:, I also have to check for absolute URLs, "/" as well as relative "../". This will be easy to fix. I will do that when I get back from cf.Objective(). Thanks.
@Toby,
Finally got around to fixing that relocation problem. Thanks for pointing it out:
www.bennadel.com/projects/cfhttp-session.htm
Absolutely Brilliant Ben, thanks for putting all the work in. Worked like a charm.
@Simon,
Glad you like!
Hi Ben,
I see, from your screenshot, that CFHTTPSession.cfc has worked against PayPal before. Apparently PayPal did not previously send cookies having special characters. Currently, PayPal is sending a cookie containing a pipe-delimited list, and another cookie having an ampersand-delimited list of name/value pairs. Thus, the pipe, ampersand, and equal sign are received as encoded characters. Running the cookies containing these new already-encoded special characters thru urlEncodedFormat, and passing them back to PayPal, prevents successful login attempt. Thus, the fix is to decode the received cookies before encoding them for sending. On line 1140 of 2010/02/05 build of CFHTTPSession.cfc (where it parses the returned cookie value), I've wrapped the value w/ urlDecode.
Thus, ListRest( LOCAL.Pair, "=" ) becomes UrlDecode(ListRest( LOCAL.Pair, "=" )).
I want to thank you very much for all the time you've invested into CFHTTPSession.cfc. Perhaps <cfhttp /> can be enhanced as thus: <cfhttp session="mySessionName" />, to allow session to be maintained amongst calls having same-named session attribute (similar to cfx_http5).
FYI, I commented out the referer spoof, and call was still successful. It seems PayPal is not (or no longer) relying on valid referer.
Thanks again!,
-Aaron Neff
@Aaron,
Very awesome detective work. I recently tried to get PayPal integration to work and was unable to get this to happen (even after starting from scratch). I'll definitely be testing this tomorrow!
@Ben,
Thanks! I thought I was losin' it..
So I did some googling and came across cfx_http5. I pretty much just implemented the code right out of their example, and was able to maintain a PayPal session just fine. So I started comparing the communication w/ Firebug and Wireshark, and also had the form post to another script on my server (instead of just PayPal). I noticed that if I pass the cookies via <cfhttpparam type="header" name="Cookie" value="semicolon delimited list of cookie name/value pairs here" /> (along w/ another header to set the content type as application/x-www-form-urlencoded), that I was able to maintain a session.
So I was almost ready to file an ER to allow cfhttpparam type="cookie" to support 'encoded="false"', but then I realized that we really should probably be unencoding the cookies, before we attempt to encode them again (which is what cfhttpparam/cookie does behind-the-scenes).
Would've been nicer if wireshark would decode the https communication w/ PayPal, but that's ok.. we got it figured out! (or I hope so.. just awaiting your confirmation)
I like that CFHTTPSession.cfc :) You really thought that out! Now to get Adobe to upgrade cfhttp to support session sharing btwn calls :)
Thanks!,
-Aaron
@Aaron,
Awesome stuff (sorry for the super late reply - had this tab open for weeks). I keep trying to remember to update my CFHTTPSession.cfc project based on your findings. Very awesome!
@Ben,
And here I am w/ another super late reply! haha, I just saw your message here from last month. Cool, the only thing I added was the urlDecode() on line 1140. Thank you, and you're welcome, and I'll be interested to know when the updated CFHTTPSession.cfc is posted.
Take care!,
-Aaron
@Aaron,
No problem at all on the late reply - I still haven't updated the project page ;) We're all busy - no judgements!
I just tried this paypal login on a openbluedragon 1.4a server and got this error:
Detail Missing Attribute: This tag requires a TYPE attribute
^ Snippet from underlying CFML source
Type Template
Message Invalid Attribute
Tag CFHTTPPARAM
Position Line=855; Column=8
Detail Missing Attribute: This tag requires a TYPE attribute
Source
852: --->
853: <cfhttpparam
854: attributecollection="#LOCAL.Param#"
855: />
856:
openBD 1.4 has support for implicit structures and arrays and also attributecollections. So I am stuck right now it should have worked. I will try some basic page scraping to see if it will work.
Great it works, at least I got it working on CF9 developer version on ubuntu after adding URLDEcode on line 1140 and changing some stuff to swedish (for the swedish site paypal.com/se).