Subdomain separation

Definitions

Admin subdomain
The main subdomain where Django-generated pages are served, where the user can perform repository management operations.
Repository subdomain
A subdomain corresponding to a Fossil repository, where pages from Fossil are proxied through and served.

Motivation

Fossil repositories are considered untrusted. Indeed, the user has infinite customization capability regarding site HTML and JS. This opens up a whole can of security worms.

Cross-site scripting

Javascript code served by a repository can perform requests on the same subdomain and read back the results as if the user were performing those requests themselves. This is an XSS (cross-side scripting) attack.

To prevent XSS attacks, each repository must thus be on a different subdomain, different from the main subdomain. The Same-Origin Policy then ensures that one repository’s JS cannot access another repository’s content, and having a different subdomain for each repository solves the XSS problem.

Cookies

Cookies have a more lax policy, unfortunately. Specifically, subdomain “a.example.com” can set a cookie for “a.example.com” and ”.example.com” (leading dot meaning wildcard).[rfc-2109]

Clearing attack

Moreover, browsers typically have an upper limit on the number of cookies they will store for each domain (regardless of subdomain); attempts to store new cookies will result in older cookies being discarded. It is trivial to write Javascript code to create thousands of junk cookies in a loop, thus clearing out all meaningful cookies for the entire domain.[github-yummy] There is apparently no way to prevent this, so there is no way for us to prevent malicious JS code from forcibly logging out users visiting a malicious repository.

Session fixation

This can be prevented by resetting the session cookie upon login. Django already does this by default it seems.

Cross-Site Request Forgery

If an untrusted Fossil subdomain knows the value of the CSRF token for the main domain or for a different untrusted Fossil domain (and the referer checking is bypassed), it can make form submissions on the trusted domain.

Obviously, we cannot use the same CSRF token on all subdomains. We need to use a different CSRF token for each subdomain, and given the token for one subdomain, it should be impossible to figure out the token for another subdomain.

A similar issue occurs with the session cookie. Untrusted subdomains should not be able to access the Django session cookie.

Server Name Indication

The server sends the requested domain/host in clear to the server (to allow it to choose which certificate to check the TLS connection against). If the subdomain contains the repository name, this is a privacy leak.

Subdomain privacy

To prevent passive attackers from finding out which repository is being visited via SNI, we obfuscate the subdomain (label) by hashing together SECRET_KEY, the repository name, and a salt value salt. If the public cookie (below) is set, seed will be taken from it. Otherwise, salt will be the request REMOTE_ADDR (this is necessary because the Fossil client does not understand cookies).

Authentication

On admin subdomain

The Django session mechanism is used, with the session cookie that is restricted to only the admin subdomain.

The Django CSRF cookie is also restricted to the admin subdomain.

On repository subdomain

To successfully authenticate on an untrusted subdomain, two signed cookies are verified:

public-subdomain-cookie
This cookie is shared among all subdomains (via wildcard domain cookie parameter).
private-subdomain-cookie
This cookie is specific to one untrusted subdomain and only valid on that subdomain. It also contains the CSRF token to be used on that subdomain.

If public-subdomain-cookie is valid but private-subdomain-cookie is absent, the middleware redirects to the https://admin.domain.tld/set-cookie?url=https://repository.domain.tld/page page on the admin subdomain, which generates and sets the private-subdomain-cookie on the appropriate subdomain, then redirects to the url parameter.

When visiting the admin subdomain, the middleware checks the presence of public-subdomain-cookie on every request. If the user is authenticated, the cookie is refreshed if absent, and if the user is unauthenticated, the cookie is cleared.

The CSRF cookie passed to Fossil (implemented via an out-of-tree patch) must be included in the private-subdomain-cookie.

[rfc-2109]https://www.ietf.org/rfc/rfc2109.txt
[github-yummy]https://blog.github.com/2013-04-09-yummy-cookies-across-domains/