防范Go语言符号链接攻击的简单有效方法

本文详细介绍了Go语言中os.Create函数存在的符号链接攻击风险,通过实际代码演示攻击原理,并提供两种有效的防护方案:使用O_NOFOLLOW标志和临时文件重命名机制,帮助开发者编写更安全的文件操作代码。

防范Go语言符号链接攻击的简单有效方法

编写Go代码多年后,我们大多已深入骨髓地掌握了错误检查模式:“这个函数是否返回错误?哦,最好在继续之前确认它是nil。”

这很好!这应该是我们编写Go代码时的默认行为。

然而,机械的错误检查有时会阻碍我们对错误实际含义的批判性思考:该函数何时返回错误?它是否包含了你认为的所有情况?

例如,在os.Create中,nil错误值可能会让你误以为文件创建是安全的。阅读相关文档后发现,os.Create实际上会在文件已存在时截断文件,而不是抛出任何指示这不是新文件的错误。

这使我们容易受到符号链接攻击。

攻击是否存在?

假设我的程序需要创建并使用一个文件。几乎所有Go代码的惯用示例都指导我们进行错误检查,但没有验证在调用Create之前文件是否已存在。如果已经为该文件设置了符号链接,不会发生错误,但由于截断行为,文件及其内容不会按预期运行。风险在于我们可以利用程序覆盖信息来删除数据。

在Trail of Bits的审计中,这个问题经常出现。幸运的是,修复方法非常简单。我们只需要在尝试创建文件之前检查文件是否存在。对我们处理惯用Go方法的一个微小调整可以使文件创建更安全,并使我们更接近在Go程序中优先考虑安全性的目标。

具体场景

假设有一个文件my_logs,我需要创建并写入。然而,在代码库的另一部分,有人之前使用ln -s other/logs my_logs设置了符号链接。

other/logs的内容:

1
2
3
4
- logs
- notes
- things I care about
- very important information we can't lose
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("my_logs")
    if err != nil {
        fmt.Printf("Error creating file: %s", err)
    }

    _, err = file.Write([]byte("My logs for this process"))
    if err != nil {
        fmt.Println(err)
    }
}

攻击演示:

1
2
3
4
5
6
$ ln -s other/logs my_logs
$ go build symlink_attack.go
$ ./symlink_attack
$ cat other/logs
- My logs for this process
$

如你所见,other/logs的内容被清除了,尽管我们的程序只与my_logs交互。

即使在这种意外场景中,os.Create也通过其截断行为删除了重要数据。在恶意场景中,攻击者可以利用截断行为针对用户删除特定数据——可能是曾经揭示他们在系统上存在的审计日志。

简单修复:两种方法

要解决这个问题,我们必须在调用Create之前插入os.IsNotExist检查。如果你运行下面编辑过的symlink_attack.goother/logs中的数据将保留且不会被覆盖。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "os"
)

func main() {

    if fileExists("my_logs") {
        log.Fatalf("symlink attack failure")
    }

    file, err := os.Create("my_logs")
    if err != nil {
        fmt.Printf("Error creating file: %s", err)
    }

    _, err = file.Write([]byte("My logs for this process"))
    if err != nil {
        fmt.Printf("Failure to write: %s", err)
    }
}

func fileExists(filename string) bool {
    info, err := os.Stat(filename)
    if os.IsNotExist(err) {
        return false
    }
    return !info.IsDir()
}

这里的限制是,通过在创建前检查os.IsNotExist,我们无法验证在存在检查和文件创建之间是否创建了符号链接(检查时间与使用时间错误)。为了解决这个问题,我们可以采取几种不同的方法。

第一种方法是使用自己的OpenFile命令重新实现os.Create,从而消除截断。

1
2
3
func Create(name string) (*File, error) {
    return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "os"
    "syscall"
)

func main() {
    file, err := os.OpenFile("my_logs", os.O_RDWR|os.O_CREATE|syscall.O_NOFOLLOW, 0666)
    if err != nil {
        log.Fatal(err)
    }

    _, err = file.Write([]byte("Is this the only thing in the file\n"))
    if err != nil {
        fmt.Printf("Failure to write: %s", err)
    }
    err = file.Close()
    if err != nil {
        fmt.Printf("Couldn't close file: %s", err)
    }
    buf, err := ioutil.ReadFile("./my_logs")
    if err != nil {
        fmt.Printf("Failed to read file: %s", err)
    }

    fmt.Printf("%s", buf)
}

通过使用O_NOFOLLOW打开文件,你将不会跟随符号链接。因此,当创建新文件时,这将与os.Create的工作方式相同。但是,如果在该位置设置了符号链接,它将无法打开。

另一种方法是创建TempFile并使用os.Rename将其移动到首选位置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "os"
)

func main() {
    tmpfile, err := ioutil.TempFile(".", "")
    if err != nil {
        log.Fatal(err)
    }

    os.Rename(tmpfile.Name(), "my_logs")
    if _, err := tmpfile.Write([]byte("Is this the only thing in the file")); err != nil {
        log.Fatal(err)
    }

    buf, err := ioutil.ReadFile("./my_logs")
    if err != nil {
        fmt.Printf("Failed to read file: %s", err)
    }

    fmt.Printf("%s", buf)
}

这种模式打破了my_logsother/logs之间的符号链接。other/logs仍然保留其内容,而my_logs只有预期的内容"Is this the only thing in the file"。

现在就保护未来的你

无论你多么小心地检查Go中的错误,它们并不总是按照你的想法行为(tl;dr:阅读手册)。但是更新你在Go文件创建中的实践非常简单,可以避免意外的后果。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计