-
Notifications
You must be signed in to change notification settings - Fork 1
/
responses.go
321 lines (286 loc) · 8.94 KB
/
responses.go
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
// Copyright © 2016,2019 Pennock Tech, LLC.
// All rights reserved, except as granted under license.
// Licensed per file LICENSE.txt
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"syscall"
"time"
)
// text should not include the newline
func (c *TCPFingerConnection) sendLine(text string) (written int64) {
pad := 2
if !c.crlf {
pad = 1
}
l := len(text)
b := make([]byte, l+pad)
copy(b, text)
if c.crlf {
b[l] = '\r'
b[l+1] = '\n'
} else {
b[l] = '\n'
}
c.conn.SetWriteDeadline(time.Now().Add(opts.requestWriteTimeout))
// stdlib net/fd_unix.go (*netFD).Write() handles short writes for us
n, err := c.conn.Write(b)
if err != nil {
c.WithError(err).WithField("wrote", n).Info("write error")
c.writeError = true
}
return int64(n)
}
func (c *TCPFingerConnection) sendOops(prefix string) (written int64) {
if prefix != "" {
return c.sendLine(fmt.Sprintf("%s: %s", prefix, "oops"))
}
return c.sendLine("oops")
}
func (c *TCPFingerConnection) processUser() (written int64) {
// don't vary the output in different scenarios:
var noSuchUserText = fmt.Sprintf("%q: no such user", c.username)
u, ok := findUser(c.username, c.Entry)
if !ok {
// caller has already set up logging context to include username= field
c.Info("unknown user")
return c.sendLine(noSuchUserText)
}
// Static files as returned from aliases bypass "owner" checks
if u.staticFile != "" {
return c.sendFile(u.staticFile, "")
}
// let sendFile apply ownership checks (symlink attacks, etc)
// (if u.uid not set, that just means no ownership checks)
c.uid = u.uid
c.homeDir = u.homeDir
if c.homeFileStat(".nofinger") != nil {
c.Info("user denies existence (.nofinger)")
return c.sendLine(noSuchUserText)
}
haveProject := c.homeFileStat(".project")
havePlan := c.homeFileStat(".plan")
havePubkey := c.homeFileStat(".pubkey")
if !(havePlan != nil || haveProject != nil || havePubkey != nil) {
c.Info("user missing finger files, denying existence")
return c.sendLine(noSuchUserText)
}
// We now will admit that the user does exist (real or alias)
written += c.sendLine(fmt.Sprintf("User: %s", c.username))
if c.writeError {
return
}
if haveProject != nil && c.homeFileValid(haveProject) {
written += c.sendFile(".project", "Project")
if c.writeError {
return
}
}
if havePlan != nil && c.homeFileValid(havePlan) {
written += c.sendFile(".plan", "Plan")
} else {
written += c.sendLine("No Plan.")
}
if c.writeError {
return
}
if havePubkey != nil && c.homeFileValid(havePubkey) {
written += c.sendFile(".pubkey", "Public key")
if c.writeError {
return
}
}
return
}
func (c *TCPFingerConnection) homeFileStat(filename string) os.FileInfo {
pathname := filepath.Join(c.homeDir, filename)
fi, err := os.Stat(pathname)
if err != nil {
if os.IsNotExist(err) || os.IsPermission(err) {
return nil
}
c.WithError(err).WithField("filename", filename).Info("unusual stat failure")
return nil
}
return fi
}
func (c *TCPFingerConnection) homeFileValid(fi os.FileInfo) bool {
if fi.Size() == 0 {
return false
}
// In code at time this comment was written, we use Stat not Lstat, so a
// ModeSymlink means that the symlink was dangling. So is invalid.
switch fi.Mode() & os.ModeType {
case 0:
break
case os.ModeSymlink:
c.WithField("filename", fi.Name()).Warn("bug in code: got a symlink result")
return false
default:
return false
}
stat, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
c.WithField("filename", fi.Name()).Warnf("bug in code for platform: stat.Sys() not Stat_t but instead %T", fi.Sys())
return false
}
// At this point, we don't have a stat on the symlink-or-file in the user's
// home directory, only the result of any symlink chains.
// This check is based upon stat of filenames, we repeat below based upon
// stat of opened files, to avoid time-of-test-to-time-of-use (TOTTTOU)
// attacks.
if stat.Uid != c.uid {
c.WithField("filename", fi.Name()).Warnf("Local user possible attack; pretending non-existent because owned %d but expected %d", stat.Uid, c.uid)
return false
}
return true
}
// sendFile returns either the amount written _or_ that nothing was written; if nothing
// was written, we treat it as not a problem as long as it's a permissions issue
func (c *TCPFingerConnection) sendFile(filename, prefix string) (written int64) {
if c.homeDir != "" && !filepath.IsAbs(filename) {
filename = filepath.Join(c.homeDir, filename)
}
// We expect the existence of the file to have already been established.
// So this should be rare; there's a risk via race if the user is mutating
// their homedir under us.
f, err := os.Open(filename)
log := c.WithField("file", filename)
if err != nil {
if os.IsPermission(err) {
log.Info("permission denied, pretending non-existent")
return 0
}
log.WithError(err).Warn("can't open to send")
return c.sendOops(prefix)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
log.WithError(err).Warn("can't stat open file-descriptor")
return c.sendOops(prefix)
}
if fi.Size() == 0 {
log.Info("pretending non-existent because file empty")
return 0
}
if fi.Size() > opts.fileSizeLimit {
log.Infof("pretending non-existent because file too large (%d > %d)", fi.Size(), opts.fileSizeLimit)
return 0
}
if fi.Mode()&os.ModeType != 0 {
log.Infof("pretending non-existent because not a file but instead: %c", fi.Mode().String()[0])
return 0
}
// c.uid set non-zero when we have an expected-user-owner
if c.uid != 0 {
stat, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
log.Warnf("pretending non-existent because bug in code for platform: stat.Sys() not Stat_t but instead %T", fi.Sys())
return 0
}
// We checked at stat time above, so if this is triggering now, it's an actual race and more malicious.
if stat.Uid != c.uid {
log.Warnf("LOCAL USER RACE ATTACK (TOTTTOU PROTECTION); pretending non-existent because owned %d but expected %d", stat.Uid, c.uid)
return 0
}
}
// should be done with safety checks, go ahead and send
eolMarker := []byte{'\r', '\n'}
if !c.crlf {
eolMarker = eolMarker[1:]
}
// we know it's presented to us as a regular file, and the size is "not too
// large", at time of stat, but the file could be open for writing
// concurrently, so we _still_ want to use a LimitReader. This will also
// protect against virtual file-systems which get sizes wrong, etc etc.
b := bufio.NewReaderSize(io.LimitReader(f, opts.fileSizeLimit), int(opts.fileSizeLimit+1))
// Page things into memory from disk before we set network write deadlines
// (while we're at it, if it's short enough, check for a newline in the first
// line, for prefix-joining)
embeddedNewline := true
func() {
if fi.Size() > 80 {
_, _ = b.Peek(int(fi.Size()))
return
}
peekAhead, _ := b.Peek(int(fi.Size()))
if !bytes.ContainsRune(peekAhead, '\n') {
embeddedNewline = false
} else if bytes.IndexRune(peekAhead, '\n') == int(fi.Size()-1) {
embeddedNewline = false
}
}()
// One deadline per file contents; we'll reset between multiple files
// for each user, as that strictly bounds how much a user can extend the
// timeout, but we don't want to deal with a slowloris reader.
c.conn.SetWriteDeadline(time.Now().Add(opts.requestWriteTimeout))
if prefix != "" {
// If the caption/prefix is short enough, we put it on one line.
// We choose (see behavior.md) to match FreeBSD's fingerd here:
// 80 - caption_length - 5; but caption there without `:`
l := len(prefix)
buf := make([]byte, l+3)
copy(buf, prefix)
buf[l] = ':'
l++
if int(fi.Size()) < (75-l) && !embeddedNewline {
buf[l] = ' '
buf = buf[:l+1]
} else if c.crlf {
buf[l] = '\r'
buf[l+1] = '\n'
} else {
buf[l] = '\n'
buf = buf[:l+1]
}
n, err := c.conn.Write(buf)
written += int64(n)
if err != nil {
log.WithError(err).Info("error writing prefix")
c.writeError = true
c.conn.SetWriteDeadline(time.Time{})
return written
}
}
for {
// ReadLine's API doesn't indicate missing final newline but that's fine,
// because we want to send one even if it's missing from the file. So it's
// "a low-level line-reading primitive. Most callers should [...]" but we're
// in the group for whom this is the right choice, I think.
chunk, isPrefix, err := b.ReadLine()
// "ReadLine either returns a non-nil line or it returns an error,
// never both." -- exception to normal Golang rule to check for content
// before handling an error.
if err != nil {
if err == io.EOF {
break
}
log.WithError(err).Info("encountered error while reading")
break
}
n, err := c.conn.Write(chunk)
written += int64(n)
if err != nil {
log.WithError(err).Infof("error returning file (wrote %d)", written)
c.writeError = true
break
}
if !isPrefix {
n, err = c.conn.Write(eolMarker)
written += int64(n)
if err != nil {
log.WithError(err).Infof("error returning file (wrote %d)", written)
c.writeError = true
break
}
}
}
c.conn.SetWriteDeadline(time.Time{})
return written
}