go-ipfs-files improperly handles writing ipfs nodes to files

Vulnerability Note

1 Summary

File: filewriter.go

The go-ipfs-files implementation of WriteTo [2] doesn’t implement any sanitisation on the names (paths) reported for file nodes. This means that an attacker can provide arbitrary paths for both symlinks, directories and files.

The ability to write to any location is a far stretching capability that an attacker can use to extract information and potentially reach remote code execution.

2 Details

2.1 Description

When a developer uses the WriteTo function they provide an IPFS node and a file path (where the target is at). The function will then procede to take any node (or subnode in case of a directory) and put it on disc, based on the Node names.

Unfortunately this leaves several opportunities for an attacker to exploit.

location: https://github.com/ipfs/go-ipfs-files/blob/master/filewriter.go#L35 The targets of symlinks are not checked, as a result it’s possible for symlinks to be created pointing to any location on the filesystem. An attacker might use this to leak data from the system where the code is executed.

Directories and Files

location: https://github.com/ipfs/go-ipfs-files/blob/master/filewriter.go#L34 The logic to write the contents of directory nodes to disk improperly assumes that the node names of it’s entries are non-malicious. Unfortunately, an attacker can use a path traversal attack to write to any location on the host. Similarly https://github.com/ipfs/go-ipfs-files/blob/master/filewriter.go#L15 also doesn’t do any input validation on fpath.

2.2 Proof of Concept

I’ve written the following test that demonstrates the vulnerability within go-ipfs-files

package files

import (
	"os"
	"testing"
)

func TestPoc(t *testing.T) {
	file, err := os.Open("sample.txt")
	if err != nil {
		t.Fatal("couldn't open sample")
	}
	st, _ := file.Stat()

	sf := NewMapDirectory(map[string]Node{
		"../see.txt": &ReaderFile{"../see.txt", file, st, -1},
	})

	WriteTo(sf, "/Users/youruser/test/go-ipfs-files/dir/")

	_, err = os.Open("/Users/walker/Targets/ipfs/go-ipfs-files/see.txt")

	if err == nil {
		t.Fatal("The path traversal succeeded")
	}
}
-- sample.txt --
this file is written to see.txt

2.3 Proposed Fix

I would propose two mitigations:

Jailing

Make sure that WriteTo can only write to & refer to files in a certain directory.

Add a parameter denoting the base directory to WriteTo. Any file, directory and symlink is only allowed to be written to this directory or one if it’s subdirectories.

Make sure that symlinks should not be allowed to target any files outside of the base directory.

Sanitisation [3]

Include node name and target sanitisation in the WriteTo function, not allowing character sequences like: ../.

2.4 Timeline

FEB/04/2021 initial disclosure to Protocol Labs;

3 References