Path Traversal in GitHub pages, and more for GitHub Enterprise


I started to write blogs (again) from early 2018. I had a blog constructed with obtvse, and added support to the mark-up syntax of Org mode (yes I’m an Emacs user) to make myself happy. But this time I was gonna try something new, like Jekyll hosted on GitHub pages, which is easy to setup and control. So I spent like 20 minutes setting up a minimal blog powered by Jekyll, then after several months when I finally had some time to look into this blog and to do some customization, the story begins.

Jekyll and symbolic links

After going through source code of Jekyll briefly I got some basic knowledge about Jekyll. Each build of a Jekyll based website consists of steps called reset read generate render cleanup write. Basically it reads all necessary files required by the build, like layouts and static resources, then renders the contents with these previously read resources. Here comes my question, Is that possible to make Jekyll unconsciously load some external file (like /etc/passwd) as a layout and use it to render some other whatever contents, so that I could smuggle the content of sensitive files to the built website.

So I created a link _layout/passwd.html -> /etc/passwd and a markdown file states to be rendered by layout passwd.

layout: passwd

at the same time, according to lib/jekyll/entry_filter.rb

    def filter(entries)
      entries.reject do |e|
        special?(e) || backup?(e) || excluded?(e) || symlink?(e) unless included?(e)

I explicitly included passwd.html in _config.yml

After the preparation, I executed jekyll build on my local machine and successfully got a rendered page _site/passwd.html having the same content as /etc/passwd.

However when I tried this on, it failed to build with the following error message:

"The symbolic link _layouts/passwd.html targets a file which does not exist within your site's repository."

oops, sounds like GitHub doesn’t like me to have a symbolic link in this repository. Can I bypass it?


According to this announcement GitHub pages supports jekyll-remote-theme plugin which allows a Jekyll site to use other hosted Jekyll theme directly, without storing a copy of the layout files in my own repository. This is an awesome feature for people who likes customization and, of course, me.

So I created another repository with the same link part _layout/passwd.html -> /etc/passwd. The repository was at nyangawa/theme. Then I modified my own _config.yml to add remote_theme: nyangawa/theme. Visiting showed me the content of GitHub’s /etc/passwd. (This website was deleted after I reported the issue. I didn’t reserve an screenshot neither :p)

However, GitHub pages service builds every site in an isolated environment (docker container). There is actually very few information to grab from it. But this is not an end yet.

GitHub Enterprise

GitHub Enterprise is a self-hosted version of GitHub. I downloaded an trial license for security assessment, so I decided to replicate this bug in GitHub Enterprise to see whether there is any difference between and GitHub Enterprise.

About deciphering the source code of GitHub Enterprise. @orangetw’s article explained this perfectly. I’m not going to explain that again, except that is part of the ruby executable in the VM since a recent version. So there is no longer require "" in the source code.

After pushing the repository to my GitHub Enterprise, I found that things are different. There’s no isolation here.


source code in /data/pages/current/script/page-build proves my guess.

# Run non-git commands as least-privledged user in dotcom
if [ -z "$SKIP_COMMAND_PREFIX" ] && [ "$PAGES_ENV" != "test" ] && [ "$PAGES_ENV" != "cucumber_test" ] && [ "$PAGES_ENV" != "development" ] && [ "$PAGES_ENV" != "enterprise" ]; then
  COMMAND_PREFIX="sudo -u jekyll"

My memory brings to me This blog by iblue. And yes I can read the session secret now.


After all, send the payload and check.


Another hit on Jekyll (Updated on 2018-10-24)

Days after the first issue, I went back to the code base of Jekyll. A strange implementation of file path checking came into my sights.

# lib/jekyll/theme.rb

    def includes_path
      @includes_path ||= path_for "_includes"

    def layouts_path
      @layouts_path ||= path_for "_layouts"

    def sass_path
      @sass_path ||= path_for "_sass"

    def assets_path
      @assets_path ||= path_for "assets"
# ...

    def path_for(folder)
      path = realpath_for(folder)
      path if path &&

    def realpath_for(folder)
      File.realpath(Jekyll.sanitized_path(root, folder.to_s))
    rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
      Jekyll.logger.warn "Invalid theme folder:", folder

These functions are used to get the real path of the resource directories (includes, layouts, etc.) in the file system. But obviously, it’s not a good idea to leave symlinks alone (again?).

Imaging if we have a link _includes -> /etc, what happens here?

  1. realpath_for returns /etc
  2. /etc passes the check in path_for and returned
  3. Jekyll will use /etc as the source directory of layouts

We successfully escaped from Jekyll’s site source again.

Being able to read any file in any directory as user git can do a lot of things. Including fetching session secret from some hidden places. :P

Some thoughts

Creating applications can be enjoyable, especially with languages like Ruby which is extremely flexible and provides many different ways to implement a feature. But what we should not forget about is that applications run on operating system. If an application wants to or has to interact with the underlay OS or file system, the developer needs to not only think the implementation from the perspective of the application, but also the perspective of the operating system, and the possibilities beneath the surface.

I’ll share some experiences about secure coding from the perspective of a system engineer. To conclude some common pitfalls where a newbie or even experienced developer could encounter.

Finally I want to thank GitHub for hosting such a responsive and efficient bug bounty program.