Client-State Manipulation
When a user interacts with a web application, they do it indirectly
through a browser. When the user clicks a button or submits a form,
the browser sends a request back to the web server. Because the
browser runs on a machine that can be controlled by an attacker, the
application must not trust any data sent by the browser.
It might seem that not trusting any user data would make it
impossible to write a web application but that's not the case. If the
user submits a form that says they wish to purchase an item, it's OK
to trust that data. But if the submitted form also includes the price
of the item, that's something that cannot be trusted.
Elevation of Privilege
Convert your account to an administrator account.
Hint 1
Take a look at
the editprofile.gtl
page that users and administrators use to edit profile settings. If you're not
an administrator, the page looks a bit different. Can you figure out
how to fool Gruyere into letting you use this page to update your account?
Hint 2
Can you figure out how to fool Gruyere into thinking you used this
page to update your account?
Exploit and Fixes
You can convert your account to being an administrator by issuing
either of the following requests:
-
https://google-gruyere.appspot.com/123/saveprofile?action=update&is_admin=True
-
https://google-gruyere.appspot.com/123/saveprofile?action=update&is_admin=True&uid=username
(which will make any username
into an an admin)
After visiting this URL, your account is now marked as an
administrator but your cookie still says you're not. So sign out and
back in to get a new cookie. After logging in, notice the
'Manage this server' link on the top right.
The bug here is that there is no validation on the server side that
the request is authorized. The only part of the code that restricts
the changes that a user is allowed to make are in the template, hiding
parts of the UI that they shouldn't have access to. The correct thing
to do is to check for authorization on the server, at the time that
the request is received.
Cookie Manipulation
Because the HTTP protocol is stateless, there's no way a web server
can automatically know that two requests are from the same user. For
this
reason,
cookies
were invented. When a web site includes a cookie (an arbitrary string)
in a HTTP response, the browser automatically sends the cookie back to
the browser on the next request. Web sites can use the cookie to save
session state. Gruyere uses cookies to remember the identity of the
logged in user. Since the cookie is stored on the client side, it's
vulnerable to manipulation. Gruyere protects the cookies from
manipulation by adding a hash to it. Notwithstanding the fact that
this hash isn't very good protection, you don't need to break the hash
to execute an attack.
Get Gruyere to issue you a cookie for someone else's account.
Hint 1
You don't need to look at the Gruyere cookie parsing code. You just need to know what the cookies look like. Gruyere's cookies use the format:
hash|username|admin|author
Hint 2
Gruyere issues a cookie when you log in. Can you trick it into
issuing you a cookie that looks like another user's cookie?
Exploit and Fix
You can get Gruyere to issue you a cookie for someone else's account
by creating a new account with
username "foo|admin|author"
. When you log into this
account, it will issue you the
cookie "hash|foo|admin|author||author"
which actually
logs you into foo
as an administrator. (So this is also
an elevation of privilege attack.)
Having no restrictions on the characters allowed in usernames
means that we have to be careful when we handle them. In this case,
the cookie parsing code is tolerant of malformed cookies and it
shouldn't be. It should escape the username when it constructs the
cookie and it should reject a cookie if it doesn't match the exact
pattern it is expecting.
Even if we fix this, Python's hash function is not
cryptographically secure. If you look at
Python's string_hash
function
in python/Objects/stringobject.cc
you'll see that it hashes the string strictly from left to right. That
means that we don't need to know the cookie secret to generate our own
hashes; all we need is another string that hashes to the same value,
which we can find in a relatively short time on a typical PC. In
contrast, with a cryptographic hash function, changing any bit of the
string will change many bits of the hash value in an unpredictable
way. At a minimum, you should use a secure hash function to protect
your cookies. You should also consider encrypting the entire cookie as
plain text cookies can expose information you might not want exposed.
And these cookies are also vulnerable to a replay attack. Once a
user is issued a cookie, it's good forever and there's no way to
revoke it. So if a user is an administrator at one time, they can save
the cookie and continue to act as an administrator even if their
administrative rights are taken away. While it's convenient to not
have to make a database query in order to check whether or not a user
is an administrator, that might be too dangerous a detail to store in
the cookie. If avoiding additional database access is important, the
server could cache a list of recent admin users. Including a timestamp
in a cookie and expiring it after some period of time also mitigates
against a replay attack.
Another challenge: Since account names are limited to 16 characters, it seems that
this trick would not work to log in to the
actual administrator
account
since "administrator|admin"
is 19 characters. Can you
figure out how to bypass that restriction?
Additional Exploit and Fix
The 16 character limit is implemented on the client side. Just issue
your own request:
https://google-gruyere.appspot.com/123/saveprofile?action=new&uid=administrator|admin|author&pw=secret
Again, this restriction should be implemented on the server side,
not just the client side.
Cross-Site Request Forgery (XSRF)
The previous section said "If the user submits a form that says they
wish to purchase an item, it's OK to trust that data." That's true as
long as it really was the user that submitted the form. If your site
is vulnerable to XSS, then the attacker can fake any request as if it
came from the user. But even if you've protected against XSS, there's
another attack that you need to protect against: cross-site request
forgery.
When a browser makes requests to a site, it always sends along any
cookies it has for that site, regardless of where the request comes
from. Additionally, web servers generally cannot distinguish between a
request initiated by a deliberate user action (e.g., user clicking on
"Submit" button) versus a request made by the browser without user
action (e.g., request for an embedded image in a page). Therefore, if
a site receives a request to perform some action (like deleting a
mail, changing contact address), it cannot know whether this action
was knowingly initiated by the user — even if the request contains
authentication cookies. An attacker can use this fact to fool the
server into performing actions the user did not intend to perform.
More details
For example, suppose Blogger is vulnerable to XSRF attacks (it
isn't). And let us say Blogger has a Delete Blog button on the
dashboard that points to this URL:
https://www.blogger.com/deleteblog.do?blogId=BLOGID
Bob, the attacker, embeds the following HTML on his web page
on
https://www.evil.example.com
:
<img src="https://www.blogger.com/deleteblog.do?blogId=alice's-blog-id"
style="display:none">
If the victim, Alice, is logged in to www.blogger.com
when
she views the above page, here is what happens:
- Her browser loads the page
from
https://www.evil.example.com
. The browser then tries to
load all embedded objects in the page, including the img
shown above.
- The browser makes a request
to
https://www.blogger.com/deleteblog.do?blogId=alice's-blog-id
to load the image. Since Alice is logged into Blogger — that is,
she has a Blogger cookie — the browser also sends that cookie in
the request.
- Blogger verifies the cookie is a valid session cookie for
Alice. It verifies that the blog referenced
by
alice's-blog-id
is owned by Alice. It deletes Alice's
blog.
- Alice has no idea what hit her.
In this sample attack, since each user has their own blog id, the
attack has to be specifically targeted to a single person. In many
cases, though, requests like these don't contain any user-specific
data.
XSRF Challenge
The goal here is to find a way to perform an account changing action
on behalf of a logged in Gruyere user without their
knowledge. Assume you can get them to visit a web page under your
control.
Find a way to get someone to delete one
of their Gruyere snippets.
Hint
What is the URL used to delete a snippet? Look at the URL associated
with the "X" next to a snippet.
Exploit and Fix
To exploit, lure a user to visit a page that makes
the following request:
https://google-gruyere.appspot.com/123/deletesnippet?index=0
To be especially sneaky, you could set your Gruyere icon to this URL
and the victim would be exploited when they visited the main page.
To fix, we should first change /deletesnippet
to
work via a POST
request since this is a state changing
action. In the HTML form, change method='get'
to method='post'
. On the server side, GET
and POST
requests look the same except that they usually
call different handlers. For example, Gruyere uses Python's
BaseHTTPServer which calls do_GET
for GET
requests and do_POST
for POST
requests.
However, note that changing to POST
is not enough
of a fix in itself! (Gruyere uses GET
requests
exclusively because it makes hacking it a bit
easier. POST
is not more secure than GET
but
it is more correct: browsers may re-issue GET
requests
which can result in an action getting executed more than once;
browsers won't reissue POST
requests without user
consent.) Then we need to pass a unique, unpredictable authorization
token to the user and require that it get sent back before performing
the action. For this authorization token, action_token
,
we can use a hash of the value of the user's cookie appended to a
current timestamp and include this token in all state-changing HTTP
requests as an additional HTTP parameter. The reason we
use POST
over GET
requests is that if we
pass action_token
as a URL parameter, it might leak via
HTTP Referer headers. The reason we include the timestamp in our hash
is so that we can expire old tokens, which mitigates the risk if it
leaks.
When a request is processed, Gruyere should regenerate the token
and compare it with the value supplied with the request. If the values
are equal, then it should perform the action. Otherwise, it should
reject it. The functions that generate and verify the tokens look like
this:
def _GenerateXsrfToken(self, cookie):
"""Generates a timestamp and XSRF token for all state changing actions."""
timestamp = time.time()
return timestamp + "|" + (str(hash(cookie_secret + cookie + timestamp)))
def _VerifyXsrfToken(self, cookie, action_token):
"""Verifies an XSRF token included in a request."""
# First, make sure that the token isn't more than a day old.
(action_time, action_hash) = action_token.split("|", 1)
now = time.time()
if now - 86400 > float(action_time):
return False
# Second, regenerate it and check that it matches the user supplied value
hash_to_verify = str(hash(cookie_secret + cookie + action_time)
return action_hash == hash_to_verify
Oops! There's several things wrong with these functions.
What's missing?
By including the time in the token, we prevent it from being used
forever, but if an attacker were to gain access to a copy of the
token, they could reuse it as many times as they wanted within that 24
hour period. The expiration time of a token should be set to a small
value that represents the reasonable length of time it will take the
user to make a request. This token also doesn't protect against an
attack where a token for one request is intercepted and then used for
a different request. As suggested by the
name action_token
, the token should be tied to the
specific state changing action being performed, such as the URL of the
page. A better signature for _GenerateXsrfToken
would
be (self, cookie, action)
. For very long actions, like
editing snippets, a script on the page could query the server to
update the token when the user hits submit. (But read the next section
about XSSI to make sure that an attacker won't be able to read that
new token.)
XSRF vulnerabilities exist because an attacker can easily script a
series of requests to an application and than force a user to execute
them by visiting some page. To prevent this type of attack, you need
to introduce some value that can't be predicted or scripted by an
attacker for every account changing request. Some application
frameworks have XSRF protection built in: they automatically include a
unique token in every response and verify it on every POST request.
Other frameworks provide functions that you can use to do that. If
neither of these cases apply, then you'll have
to build your own. Be careful of things that don't
work: using POST
instead of GET
is advisable
but not sufficient by itself, checking Referer headers is
insufficient, and copying cookies into hidden form fields can make
your cookies less secure.
Cross Site Script Inclusion (XSSI)
Browsers prevent pages of one domain from reading pages in other
domains. But they do not prevent pages of a domain from referencing
resources in other domains. In particular, they allow images to be
rendered from other domains and scripts to be executed from other
domains. An included script doesn't have its own security context. It
runs in the security context of the page that included it. For
example, if www.evil.example.com
includes a script hosted
on www.google.com
then that script runs in
the evil
context not in the google
context. So any user data in that script will "leak."
XSSI Challenge
Find a way to read someone else's private snippet using XSSI.
That is, create a page on another web site and put something in
that page that can read your private snippet. (You don't need to post
it to a web site: you can just create a .html
in your
home directory and double click on it to open in a browser.)
Hint 1
You can run a script from another domain by
adding
<SCRIPT src="https://google-gruyere.appspot.com/123/..."></SCRIPT>
to your HTML file. What scripts does Gruyere have?
Hint 2
feed.gtl
is a
script. Given that, how can you get the private snippet out of the
script?
Exploit and Fix
To exploit, put this in an html file:
<script>
function _feed(s) {
alert("Your private snippet is: " + s['private_snippet']);
}
</script>
<script src="https://google-gruyere.appspot.com/123/feed.gtl"></script>
When the script
in feed.gtl
is
executed, it runs in the context of the attacker's web page and uses
the _feed
function which can do whatever it wants with
the data, including sending it off to another web site.
You might think that you can fix this by eliminating the function
call and just having the bare expression. That way, when the script is
executed by inclusion, the response will be evaluated and then
discarded. That won't work because JavaScript allows you to do things
like redefine default constructors. So when the object is evaluated,
the hosting page's constructors are invoked, which can do whatever
they want with the values.
To fix, there are several changes you can make. Any
one of these changes will prevent currently possible attacks, but if
you add several layers of protection
("defense
in depth") you protect against the possibility that you get one of
the protections wrong and also against future browser
vulnerabilities. First, use an XSRF token as discussed earlier to make
sure that JSON results containing confidential data are only returned
to your own pages. Second, your JSON response pages should only
support POST
requests, which prevents the script from
being loaded via a script tag. Third, you should make sure that the
script is not executable. The standard way of doing this is to append
some non-executable prefix to it,
like ])}while(1);</x>
. A script running in the same
domain can read the contents of the response and strip out the prefix,
but scripts running in other domains can't.
NOTE: Making the script not executable is more subtle than it
seems. It's possible that what makes a script executable may change in
the future if new scripting features or languages are introduced. Some
people suggest that you can protect the script by making it a comment
by surrounding it with /*
and */
, but that's
not as simple as it might seem. (Hint: what if someone
included */
in one of their snippets?)
There's much more to XSSI than this. There's a variation of
JSON called JSONP which you should avoid using because it allows
script injection by design. And
there's E4X (Ecmascript for XML) which can result in your
HTML file being parsed as a script. Surprisingly, one way to protect
against E4X attacks is to put some invalid XML in your files, like
the </x>
above.
Continue >>
© Google 2017 Terms of Service
The code portions of this codelab are licensed under the
Creative Commons Attribution-No Derivative Works 3.0 United States license
<https://creativecommons.org/licenses/by-nd/3.0/us>.
Brief excerpts of the code may be used for educational or
instructional purposes provided this notice is kept intact.
Except as otherwise noted the remainder of this codelab is licensed under the
Creative Commons Attribution 3.0 United States license
<https://creativecommons.org/licenses/by/3.0/us>.