Why “Behind the Firewall” isn’t good enough

Firewalls are everywhere. They filter unexpected traffic, and log everything for later analysis. They improve security by restricting access to potentially vulnerable software that only needs to be accessed internally. They proactively filter ports and services which may have undiscovered vulnerabilities. They prevent information gathering which might aid a future attack. They’re relatively easy to deploy and don’t require as much specialized knowledge as other security methods. Firewalls work.

Firewalls are a crutch. They allow systems and network administrators to say that a network is secure without spending the time to secure each machine and component. They mean that the sysadmin may choose not to disable every unused service and port on a machine. They mean that companies use unprotected file shares rather than authenticating each user and encrypting the protocol. They allow administrators to deploy applications which are not secure, with the explanation that the firewall will make them so.

This has historically worked pretty well. Firewalls do work, after all.

Today the situation is different.

While firewalls are very good at blocking all kinds of inbound traffic, they necessarily allow outbound web traffic to pass through. Internet access is a requirement for most jobs, and so administrators install antivirus, keep desktop software up to date, and hope that they don’t get a motivated attacker with a new 0-day.

This also works pretty well, except for one thing:

A properly functioning, fully patched web browser is an ideal gateway to internal web-based assets that are “Behind the Firewall”. By design, the browser is able to access content from multiple domains, and the rise of javascript (and now the various features of HTML5) allow a remote attacker to pass requests through the firewall to be made internally by trusted machines. Frameworks like BeEF make this tunneling trivial. Add a little DNS trickery and a browser can be convinced to do pretty advanced scans of private networks.

Web applications deployed behind the firewall should be secure enough to deploy facing the internet. This is especially true when the code is written for internal consumption by programmers with a deadline. The firewall can prevent many evils, but it won’t prevent web browsers from behaving as designed.

Djangocon talk

I’m pleased to announce that I will be giving a talk about Advanced Security with Django at this year’s Djangocon in Portland.

This talk will introduce several advanced security topics, and discuss how Django fares. Topics will include timing attacks, man-in-the-middle, hashing issues, brute force attacks, and several other topics that can’t currently be discussed (pending fixes in core). Expect practical demonstrations of “theoretical” vulnerabilities.

The second half of the talk will focus on how we can improve Django’s security in the future. How can we improve response time and transparency for security issues? How can we make it easier to provide security enhancements for new code while retaining backwards compatibility? How can the community support security work that is low on the priority list for current core devs?

Let me know if there are other specific topics you want to see covered.

 

Django Directory traversal vulnerability with file-based sessions on Windows

Original announcement:

https://www.djangoproject.com/weblog/2011/feb/08/security/

When Django is used with the included file-based session storage
backend on Windows, it is vulnerable to a potential directory
traversal attack.

In django.contrib.sessions.backends.file._key_to_file():

36     # Make sure we're not vulnerable to directory traversal. Session keys
37     # should always be md5s, so they should never contain directory
38     # components.
39     if os.path.sep in session_key:
40         raise SuspiciousOperation(
41             "Invalid characters (directory components) in session key")
42
43      return os.path.join(self.storage_path, self.file_prefix + session_key)

Django takes a user supplied session key, checks to make sure it does
not contain os.path.sep, and then uses os.path.join() to create a
filename string which is later opened and unpickled to restore session
data.

The vulnerability lies in the check against os.path.sep. On Unix-like
systems, this returns the / character, which is the only standard way
to specify a directory separator. On Windows, this returns the \
character (the usual DOS path separator). The problem is that Windows
(and so Python) has always accepted either character as a path
separator [1].

This means that a string which only uses forward slashes does not trip
the Suspicious Operation check on Windows. Django happily accepts a
cookie with ‘sessionid=../../../maliciousfile’. It looks for the file,
and will follow the usual session restore procedures if it finds a
file at the user-supplied path.

Fortunately, Django hashes and verifies the contents of sessions
loaded in this manner. This means that without breaking the checksum
mechanism, the best an attacker is likely to be able to accomplish is
some kind of session replay attack where they are able to re-insert a
formerly valid session at will. This might be useful against systems
which store permissions or “value” of some kind in the session. If the
attacker is able to break the checksum mechanism [2], this attack
could lead to arbitrary code execution when pickle loads the stored
session.

The patch arrived in changeset 15476.

[1] You can’t usually type the / into a DOS command line as a
separator because it is set as the switch character. You can run the
DOS shell with a different switch character if you like.

[2] There are a bunch of known attacks on MD5. To the best of my
knowledge, the particular ways that Django uses it in this case are
not currently vulnerable to the easy attacks.

DoS issue in contrib.auth password reset mechanism in Django

Here’s the original announcement on the Django blog:

https://www.djangoproject.com/weblog/2010/dec/22/security/

The relevant code lives here:

http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/tokens.py

When a user requests a password reset, they enter their email and a
token is sent with a url of the form:
http://example.com/reset/(userid)-(base36_timestamp)-(hash)/

Generally, the timestamp portion is exactly 3 digits, and represents
the expiration date for the overall token. During processing, this
gets parsed into an integer, and is then parsed back to base36 so that
it can be rehashed to compare the expected hash against the user
provided hash.

The issue is that we don’t prevent users from inserting extremely
large values into this field. Depending on server configuration, this
may allow values starting from ~10^6000 (the longest url apache allows
by default) to values in excess of 10^12500.

Obviously, base conversion and hashing operations on numbers this
large tend to use many resources.

The exploit is simple – construct a url of the form:

http://example.com/reset/1-zzzz . . . zzzz-1/

Use as many z’s as your target server will allow, and hit the server a
few times. On my test Apache installation, the longest allowable
string pegged the cpu for around 4 seconds per request (4032
z’s)[1,2]. With lighttpd where the url length limit was higher, I was
able to peg the cpu for more than 60 seconds per request. Obviously
the dev server also displays this problem.

The severity of the DoS is related to the configuration of the server.
Longer allowable urls equate to worse vulnerability. Many servers
default to allowing extremely long urls. Multithreading doesn’t negate
the problem much since the client doesn’t have to stick around while
the request is returning.

To replicate, create a new project, enable the admin, add at least one
user, and import the urlpatterns from django.contrib.auth.urls in your
url config.

The patch arrived in changeset 15032.

[1] I was using the default values on Ubuntu. Other systems may have a
higher default URL length limit.

[2] For some reason, the user agent seems to influence how severely
Apache is affected. Using Firefox produced a significantly lower
impact than using the command line tool lwp-request (aliased to “GET”
on my machine).