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/ |