Parsia's Den

The knowledge of anything, since all things have causes, is not acquired or complete unless it is known by its causes. - Avicenna

Nov 10, 2018 - 3 minute read - Comments - Go

filepath.Ext Notes

The filepath package has some functions for processing paths and filenames. I am using it extensively in a current project. You can do cool stuff with it, like traversing a path recursively with filepath.Walk.

filepath.Ext returns the extension of a filename (or path). It returns whatever is after the last dot. It has some gotchas that might have security implications.

Code is at: https://github.com/parsiya/Parsia-Code/tree/master/filepath-ext

Source

https://golang.org/src/path/filepath/path.go?s=6131:6159#L207

// Ext returns the file name extension used by path.
// The extension is the suffix beginning at the final dot
// in the final element of path; it is empty if there is
// no dot.
func Ext(path string) string {
	for i := len(path) - 1; i >= 0 && !os.IsPathSeparator(path[i]); i-- {
		if path[i] == '.' {
			return path[i:]
		}
	}
	return ""
}

Example

To demonstrate the tips, we are going to create a simple example. Let’s assume we have a file hosting service. We have created a blacklist of banned extensions. It checks the files by extension. This is not really a good way to do this:

  1. It’s better to default to deny and then use a whitelist.
  2. Detecting file types by extension is easily bypassed by changing the extension. However, this might render some attacks invalid. Consider an attack where a user downloads a file, if the attacker has been forced to rename the file from exe to txt or another random extension, the user needs to manually rename it back and execute it.

But let’s use it for demonstrating our points. This is the blacklist function:

// Blacklist returns true if a file is one of the banned types by checking its extension.
func Blacklist(filename string) bool {
	if filepath.Ext(filename) == ".exe" {
		return false
	}
	return true
}

Ext Keeps the Case of Input

The input’s letter case does not change. Everything is just passed through. Consider the following code (case.go):

package main

import (
	"fmt"
	"path/filepath"
)

func main() {
    fmt.Println("filepath.Ext(\"whatever.txt\"):", filepath.Ext("whatever.txt"))
    // filepath.Ext("whatever.txt"): .txt

    fmt.Println("filepath.Ext(\"whatever.TXT\"):", filepath.Ext("whatever.TXT"))
    // filepath.Ext("whatever.TXT"): .TXT

    fmt.Println("filepath.Ext(\"whatever.Txt\"):", filepath.Ext("whatever.Txt"))
    // filepath.Ext("whatever.Txt"): .Txt
}

To bypass the blacklist, we can just change the case case_blacklist.go:

func main() {
    fmt.Println("Blacklist(\"whatever.exe\"):", Blacklist("whatever.exe"))
    // true

    fmt.Println("Blacklist(\"whatever.ExE\"):", Blacklist("whatever.ExE"))
    // false
}

Ext Returns the dot with the Extension

In our mind, the extension of a file is just the letters after the dot. It does not include the dot. However, Ext returns the dot. This is really unintuitive and has caught me off guard a few times.

package main

import (
	"fmt"
	"path/filepath"
)

func main() {
    fmt.Println("Blacklist(\"whatever.exe\"):", Blacklist("whatever.exe"))
    // true
}

// Blacklist returns true if a file is one of the banned types by checking its extension.
func Blacklist(filename string) bool {
	// Developers did not expect the dot to be part of the output.
	if filepath.Ext(filename) == "exe" {
		return false
	}
	return true
}

This blacklist is useless, the condition is never true.

Returns an Empty String if Input has no Dots

This is mentioned in the docs but aI did not expect it.