@@ -22,6 +22,7 @@ import (
2222 "encoding/json"
2323 "errors"
2424 "fmt"
25+ "io"
2526 "math"
2627 "os"
2728 "path/filepath"
@@ -931,7 +932,12 @@ func WithAppendAdditionalGroups(groups ...string) SpecOpts {
931932 if err != nil {
932933 return err
933934 }
934- ugroups , groupErr := user .ParseGroupFile (gpath )
935+ var ugroups []user.Group
936+ f , groupErr := openBoundedUserFile (gpath )
937+ if groupErr == nil {
938+ ugroups , groupErr = user .ParseGroup (f )
939+ f .Close ()
940+ }
935941 if groupErr != nil && ! os .IsNotExist (groupErr ) {
936942 return groupErr
937943 }
@@ -1091,7 +1097,12 @@ func UserFromPath(root string, filter func(user.User) bool) (user.User, error) {
10911097 if err != nil {
10921098 return user.User {}, err
10931099 }
1094- users , err := user .ParsePasswdFileFilter (ppath , filter )
1100+ f , err := openBoundedUserFile (ppath )
1101+ if err != nil {
1102+ return user.User {}, err
1103+ }
1104+ defer f .Close ()
1105+ users , err := user .ParsePasswdFilter (f , filter )
10951106 if err != nil {
10961107 return user.User {}, err
10971108 }
@@ -1111,7 +1122,12 @@ func GIDFromPath(root string, filter func(user.Group) bool) (gid uint32, err err
11111122 if err != nil {
11121123 return 0 , err
11131124 }
1114- groups , err := user .ParseGroupFileFilter (gpath , filter )
1125+ f , err := openBoundedUserFile (gpath )
1126+ if err != nil {
1127+ return 0 , err
1128+ }
1129+ defer f .Close ()
1130+ groups , err := user .ParseGroupFilter (f , filter )
11151131 if err != nil {
11161132 return 0 , err
11171133 }
@@ -1127,7 +1143,12 @@ func getSupplementalGroupsFromPath(root string, filter func(user.Group) bool) ([
11271143 if err != nil {
11281144 return []uint32 {}, err
11291145 }
1130- groups , err := user .ParseGroupFileFilter (gpath , filter )
1146+ f , err := openBoundedUserFile (gpath )
1147+ if err != nil {
1148+ return []uint32 {}, err
1149+ }
1150+ defer f .Close ()
1151+ groups , err := user .ParseGroupFilter (f , filter )
11311152 if err != nil {
11321153 return []uint32 {}, err
11331154 }
@@ -1671,3 +1692,58 @@ func WithWindowsNetworkNamespace(ns string) SpecOpts {
16711692 return nil
16721693 }
16731694}
1695+
1696+ // maxUserFileBytes caps how much data is read from any user-database file
1697+ // opened via openBoundedUserFile. Real systems keep these files well under
1698+ // 1 MiB; 10 MiB is generous headroom while keeping peak memory during
1699+ // user.ParsePasswd/ParseGroup bounded to single-digit MiB.
1700+ const maxUserFileBytes = 10 << 20
1701+
1702+ // openBoundedUserFile opens path and returns an io.ReadCloser that errors out
1703+ // if more than maxUserFileBytes are read from it. Non-regular sources are
1704+ // rejected before opening, so callers never block on FIFOs or device files
1705+ // and parsers never consume bytes from them.
1706+ //
1707+ // openBoundedUserFile does NOT perform any path validation. It does not guard
1708+ // against symlink traversal or paths that escape the container rootfs, and it
1709+ // follows symlinks both when stat-ing and when opening. Callers are responsible
1710+ // for confining path to the intended root beforehand (e.g. via fs.RootPath,
1711+ // which resolves every symlink component and re-anchors absolute links to the
1712+ // root) and must not pass attacker-controlled, unresolved paths directly.
1713+ func openBoundedUserFile (path string ) (io.ReadCloser , error ) {
1714+ info , err := os .Stat (path )
1715+ if err != nil {
1716+ return nil , err
1717+ }
1718+ if ! info .Mode ().IsRegular () {
1719+ return nil , fmt .Errorf ("%s is not a regular file" , path )
1720+ }
1721+ f , err := os .Open (path )
1722+ if err != nil {
1723+ return nil , err
1724+ }
1725+ return & limitedFile {
1726+ Closer : f ,
1727+ // Allow one byte past the cap so an overflow surfaces as an
1728+ // error rather than a silent EOF that the parser would treat as
1729+ // a clean end-of-file (and miss any entries past the cap).
1730+ r : & io.LimitedReader {R : f , N : maxUserFileBytes + 1 },
1731+ name : path ,
1732+ }, nil
1733+ }
1734+
1735+ // limitedFile is an io.ReadCloser whose Read returns an error once more than
1736+ // maxUserFileBytes have been read.
1737+ type limitedFile struct {
1738+ io.Closer
1739+ r * io.LimitedReader
1740+ name string
1741+ }
1742+
1743+ func (l * limitedFile ) Read (p []byte ) (int , error ) {
1744+ n , err := l .r .Read (p )
1745+ if l .r .N == 0 {
1746+ return n , fmt .Errorf ("%q exceeds %d bytes" , l .name , maxUserFileBytes )
1747+ }
1748+ return n , err
1749+ }
0 commit comments