3
3
package utils
4
4
5
5
import (
6
+ "errors"
6
7
"fmt"
7
8
"math"
8
9
"os"
@@ -13,6 +14,8 @@ import (
13
14
"sync"
14
15
_ "unsafe" // for go:linkname
15
16
17
+ "github.com/opencontainers/runc/libcontainer/system"
18
+
16
19
securejoin "github.com/cyphar/filepath-securejoin"
17
20
"github.com/sirupsen/logrus"
18
21
"golang.org/x/sys/unix"
@@ -275,3 +278,112 @@ func IsLexicallyInRoot(root, path string) bool {
275
278
}
276
279
return strings .HasPrefix (path , root )
277
280
}
281
+
282
+ // MkdirAllInRootOpen attempts to make
283
+ //
284
+ // path, _ := securejoin.SecureJoin(root, unsafePath)
285
+ // os.MkdirAll(path, mode)
286
+ // os.Open(path)
287
+ //
288
+ // safer against attacks where components in the path are changed between
289
+ // SecureJoin returning and MkdirAll (or Open) being called. In particular, we
290
+ // try to detect any symlink components in the path while we are doing the
291
+ // MkdirAll.
292
+ //
293
+ // NOTE: Unlike os.MkdirAll, mode is not Go's os.FileMode, it is the unix mode
294
+ // (the suid/sgid/sticky bits are not the same as for os.FileMode).
295
+ //
296
+ // NOTE: If unsafePath is a subpath of root, we assume that you have already
297
+ // called SecureJoin and so we use the provided path verbatim without resolving
298
+ // any symlinks (this is done in a way that avoids symlink-exchange races).
299
+ // This means that the path also must not contain ".." elements, otherwise an
300
+ // error will occur.
301
+ //
302
+ // This is a somewhat less safe alternative to
303
+ // <https://github.com/cyphar/filepath-securejoin/pull/13>, but it should
304
+ // detect attempts to trick us into creating directories outside of the root.
305
+ // We should migrate to securejoin.MkdirAll once it is merged.
306
+ func MkdirAllInRootOpen (root , unsafePath string , mode uint32 ) (_ * os.File , Err error ) {
307
+ // If the path is already "within" the root, use it verbatim.
308
+ fullPath := unsafePath
309
+ if ! IsLexicallyInRoot (root , unsafePath ) {
310
+ var err error
311
+ fullPath , err = securejoin .SecureJoin (root , unsafePath )
312
+ if err != nil {
313
+ return nil , err
314
+ }
315
+ }
316
+ subPath , err := filepath .Rel (root , fullPath )
317
+ if err != nil {
318
+ return nil , err
319
+ }
320
+
321
+ // Check for any silly mode bits.
322
+ if mode &^0o7777 != 0 {
323
+ return nil , fmt .Errorf ("tried to include non-mode bits in MkdirAll mode: 0o%.3o" , mode )
324
+ }
325
+
326
+ currentDir , err := os .OpenFile (root , unix .O_DIRECTORY | unix .O_CLOEXEC , 0 )
327
+ if err != nil {
328
+ return nil , fmt .Errorf ("open root handle: %w" , err )
329
+ }
330
+ defer func () {
331
+ if Err != nil {
332
+ currentDir .Close ()
333
+ }
334
+ }()
335
+
336
+ for _ , part := range strings .Split (subPath , string (filepath .Separator )) {
337
+ switch part {
338
+ case "" , "." :
339
+ // Skip over no-op components.
340
+ continue
341
+ case ".." :
342
+ return nil , fmt .Errorf ("possible breakout detected: found %q component in SecureJoin subpath %s" , part , subPath )
343
+ }
344
+
345
+ nextDir , err := system .Openat (currentDir , part , unix .O_DIRECTORY | unix .O_NOFOLLOW | unix .O_CLOEXEC , 0 )
346
+ switch {
347
+ case err == nil :
348
+ // Update the currentDir.
349
+ _ = currentDir .Close ()
350
+ currentDir = nextDir
351
+
352
+ case errors .Is (err , unix .ENOTDIR ):
353
+ // This might be a symlink or some other random file. Either way,
354
+ // error out.
355
+ return nil , fmt .Errorf ("cannot mkdir in %s/%s: %w" , currentDir .Name (), part , unix .ENOTDIR )
356
+
357
+ case errors .Is (err , os .ErrNotExist ):
358
+ // Luckily, mkdirat will not follow trailing symlinks, so this is
359
+ // safe to do as-is.
360
+ if err := system .Mkdirat (currentDir , part , mode ); err != nil {
361
+ return nil , err
362
+ }
363
+ // Open the new directory. There is a race here where an attacker
364
+ // could swap the directory with a different directory, but
365
+ // MkdirAll's fuzzy semantics mean we don't care about that.
366
+ nextDir , err := system .Openat (currentDir , part , unix .O_DIRECTORY | unix .O_NOFOLLOW | unix .O_CLOEXEC , 0 )
367
+ if err != nil {
368
+ return nil , fmt .Errorf ("open newly created directory: %w" , err )
369
+ }
370
+ // Update the currentDir.
371
+ _ = currentDir .Close ()
372
+ currentDir = nextDir
373
+
374
+ default :
375
+ return nil , err
376
+ }
377
+ }
378
+ return currentDir , nil
379
+ }
380
+
381
+ // MkdirAllInRoot is a wrapper around MkdirAllInRootOpen which closes the
382
+ // returned handle, for callers that don't need to use it.
383
+ func MkdirAllInRoot (root , unsafePath string , mode uint32 ) error {
384
+ f , err := MkdirAllInRootOpen (root , unsafePath , mode )
385
+ if err == nil {
386
+ _ = f .Close ()
387
+ }
388
+ return err
389
+ }
0 commit comments