Skip to Content
OpenVAPTAlpha
Pre-release. Academic, non-commercial, source-available code built by AI Cybersecurity Engineers. Free for your own use, 100% at your own risk. This website collects no data. Read MoreRead Less
Pre-release. OpenVAPT has no Stable release yet. Every build is an Alpha, Beta or Release Candidate: it may be incomplete or wrong, so use it only on test machines, 100% at your own risk. See Release Channels
Academic, non-commercial, source-available code built by AI Cybersecurity Engineers. These are AI systems, not people or registered engineers. Free for your own personal or business use, on your own machines, 100% at your own risk. Not for sale. Not for providing services. This website collects no data. Read the Acknowledgements
Pre-release. OpenVAPT has no Stable release yet. Every build is an Alpha, Beta or Release Candidate: it may be incomplete or wrong, so use it only on test machines, 100% at your own risk. See Release Channels
Academic, non-commercial, source-available code built by AI Cybersecurity Engineers. These are AI systems, not people or registered engineers. Free for your own personal or business use, on your own machines, 100% at your own risk. Not for sale. Not for providing services. This website collects no data. Read the Acknowledgements
HighHOST-FS-005Access ControlServer

Cron Files Writable by Non-Root Users

Checks that system cron files and directories are owned by root and not writable by others.

Why It Matters
Cron runs these jobs as root. Anyone who can edit them can run any command as root at the next scheduled minute.
How It Detects
Stats /etc/crontab, /etc/cron.d and its files, and the cron.hourly/daily/weekly/monthly directories, flagging non-root owners or group/other write bits.
How to Fix
chown root:root <path> && chmod go-w <path> for each listed path (600 for /etc/crontab is a common hardening choice).
How to Verify
stat -c '%a %U %n' /etc/crontab /etc/cron.*
CVSS v3.1 Vector
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Control Category
Access Control: CIS Controls v8 3, 6; NIST CSF 2.0 PR.AA, PR.DS
Fix Effort
Low
CIS Controls v8 Safeguard
3.3
NIST CSF 2.0 Subcategory
PR.AA-05, PR.DS-01
CWE
CWE-732
NIST SP 800-53 Rev. 5
AC-3, CM-6
Benchmark Topic
Job schedulers: cron permissions (topic only, no benchmark text)
References
crontab(5), Linux man-pages
MITRE ATT&CK T1053.003, Cron
Learn the theory behind this check: P1 Linux Server Hardening.

Source Code

The complete file that defines this check. Run it with openvapt scan --only HOST-FS-005, or read it offline with openvapt explain HOST-FS-005.

internal/checks/host/files.go · Lines 1-244View on GitHub at This Commit
1package host
2
3import (
4 "context"
5 "fmt"
6 "io/fs"
7 "sort"
8
9 "github.com/openvapt/openvapt/internal/check"
10 "github.com/openvapt/openvapt/internal/sys"
11)
12
13func perm(m fs.FileMode) string { return fmt.Sprintf("%04o", uint32(m.Perm())|specialBits(m)) }
14
15func specialBits(m fs.FileMode) uint32 {
16 var b uint32
17 if m&fs.ModeSetuid != 0 {
18 b |= 0o4000
19 }
20 if m&fs.ModeSetgid != 0 {
21 b |= 0o2000
22 }
23 if m&fs.ModeSticky != 0 {
24 b |= 0o1000
25 }
26 return b
27}
28
29func init() {
30 check.Register(&check.Check{
31 ID: "HOST-FS-001",
32 Title: "Sensitive Account Files Have Unsafe Permissions",
33 Domain: check.DomainHost,
34 Severity: check.High,
35 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N",
36 Summary: "Checks that /etc/shadow and /etc/gshadow are not readable by others, and /etc/passwd and /etc/group are not writable by others.",
37 Rationale: "Readable shadow files let any local user (or a compromised web app) copy password hashes and crack them offline. Writable passwd/group files let them create a root account.",
38 Detection: "Stats each file without reading its contents and inspects the permission bits for 'other' and 'group' access.",
39 Fix: "chown root:root /etc/passwd /etc/group && chmod 644 /etc/passwd /etc/group ; chown root:shadow /etc/shadow /etc/gshadow && chmod 640 /etc/shadow /etc/gshadow (use root:root and 000 on RHEL-family systems).",
40 Verify: "stat -c '%a %U:%G %n' /etc/passwd /etc/group /etc/shadow /etc/gshadow",
41 Refs: []check.Ref{{Title: "shadow(5), Linux man-pages", URL: "https://man7.org/linux/man-pages/man5/shadow.5.html"}, refNIST800123},
42 Standards: check.Standards{CWE: []string{"CWE-732"}, NIST: []string{"AC-3", "AC-6"}, CISTopic: "File permissions: account database files"},
43 Run: func(_ context.Context, s sys.System) []check.Result {
44 var out []check.Result
45 for _, f := range []struct {
46 path string
47 bad fs.FileMode
48 why string
49 }{
50 {"/etc/shadow", 0o004 | 0o002, "readable or writable by others"},
51 {"/etc/gshadow", 0o004 | 0o002, "readable or writable by others"},
52 {"/etc/passwd", 0o022, "writable by group or others"},
53 {"/etc/group", 0o022, "writable by group or others"},
54 } {
55 st, err := s.Stat(f.path)
56 if err != nil {
57 continue
58 }
59 if st.Mode.Perm()&f.bad != 0 {
60 out = append(out, check.FailR(f.path, f.path+" is "+f.why, "mode "+perm(st.Mode)))
61 } else {
62 out = append(out, check.PassR(f.path, f.path+" permissions are safe", "mode "+perm(st.Mode)))
63 }
64 }
65 if len(out) == 0 {
66 return []check.Result{check.SkipR("/etc", "Account files not found")}
67 }
68 return out
69 },
70 })
71
72 check.Register(&check.Check{
73 ID: "HOST-FS-002",
74 Title: "Temporary Directory Missing the Sticky Bit",
75 Domain: check.DomainHost,
76 Severity: check.High,
77 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H",
78 Summary: "Checks that world-writable temp directories have the sticky bit set.",
79 Rationale: "Without the sticky bit any user can delete or replace other users' files in /tmp, enabling symlink attacks against root-run scripts.",
80 Detection: "Stats /tmp and /var/tmp; world-writable without the sticky bit (mode 1777 expected) fails.",
81 Fix: "chmod 1777 /tmp /var/tmp",
82 Verify: "stat -c '%a %n' /tmp /var/tmp should print 1777 for both.",
83 Refs: []check.Ref{{Title: "chmod(1), Linux man-pages (restricted deletion flag)", URL: "https://man7.org/linux/man-pages/man1/chmod.1.html"}},
84 Standards: check.Standards{CWE: []string{"CWE-377", "CWE-732"}, NIST: []string{"AC-3"}, CISTopic: "Filesystem: sticky bit on world-writable directories"},
85 Run: func(_ context.Context, s sys.System) []check.Result {
86 var out []check.Result
87 for _, d := range []string{"/tmp", "/var/tmp"} {
88 st, err := s.Stat(d)
89 if err != nil {
90 continue
91 }
92 if st.Mode.Perm()&0o002 != 0 && st.Mode&fs.ModeSticky == 0 {
93 out = append(out, check.FailR(d, d+" is world-writable without the sticky bit", "mode "+perm(st.Mode)))
94 } else {
95 out = append(out, check.PassR(d, d+" is protected", "mode "+perm(st.Mode)))
96 }
97 }
98 return out
99 },
100 })
101
102 check.Register(&check.Check{
103 ID: "HOST-FS-003",
104 Title: "World-Writable Files in System Configuration",
105 Domain: check.DomainHost,
106 Severity: check.High,
107 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
108 Summary: "Looks for files under /etc and in cron directories that any user can modify.",
109 Rationale: "A world-writable config or cron file lets any local user change how a root-owned service behaves, often leading straight to root.",
110 Detection: "Walks /etc (metadata only, up to 20000 files, symlinks not followed) and lists regular files with the 'other write' bit set, plus directories up to three levels deep that are world-writable without the sticky bit (anyone could replace files inside them).",
111 Fix: "For each listed file or directory: chmod o-w <path>. Then find out how it became writable (a deployment script or a careless chmod 777).",
112 Verify: "find /etc -xdev \\( -type f -perm -0002 \\) -o \\( -type d -perm -0002 ! -perm -1000 \\) should print nothing.",
113 Refs: []check.Ref{refNIST800123},
114 Standards: check.Standards{CWE: []string{"CWE-732"}, NIST: []string{"AC-3", "CM-6"}, CISTopic: "File permissions: no world-writable files"},
115 Run: func(ctx context.Context, s sys.System) []check.Result {
116 walk, err := s.Walk(ctx, "/etc", 20000)
117 files := walk.Files
118 if err != nil && len(files) == 0 {
119 return []check.Result{check.ErrR("/etc", err)}
120 }
121 var bad []string
122 for _, f := range files {
123 if f.Mode.Perm()&0o002 != 0 {
124 bad = append(bad, perm(f.Mode)+" "+f.Path)
125 }
126 }
127 bad = append(bad, worldWritableDirs(s, "/etc", 3, 5000)...)
128 if len(bad) > 0 {
129 if len(bad) > 25 {
130 bad = append(bad[:25], fmt.Sprintf("... and %d more", len(bad)-25))
131 }
132 return []check.Result{check.FailR("/etc", "World-writable files found", bad...)}
133 }
134 if !walk.Complete {
135 return []check.Result{check.IncompleteR("/etc", fmt.Sprintf("Filesystem walk was partial after %d files", len(files)))}
136 }
137 return []check.Result{check.PassR("/etc", fmt.Sprintf("No world-writable files among %d checked", len(files)))}
138 },
139 })
140
141 check.Register(&check.Check{
142 ID: "HOST-FS-004",
143 Title: "Unexpected SUID or SGID Programs",
144 Domain: check.DomainHost,
145 Severity: check.High,
146 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
147 Summary: "Looks for set-user-ID or set-group-ID programs in places where packages do not normally install them.",
148 Rationale: "A SUID program runs with its owner's rights (usually root). One placed in /tmp, /home, /var/www or /opt is a common persistence and privilege-escalation trick.",
149 Detection: "Walks /tmp, /var/tmp, /dev/shm, /home, /var/www, /srv, /opt and /usr/local (metadata only) and lists files with the SUID or SGID bit.",
150 Fix: "Confirm whether each program is needed. Remove the bit with chmod u-s,g-s <file>, or delete the file if you did not put it there and investigate how it arrived.",
151 Verify: "find /tmp /home /var/www /opt /usr/local -xdev -perm /6000 -type f should print nothing unexpected.",
152 Refs: []check.Ref{{Title: "MITRE ATT&CK T1548.001, Setuid and Setgid", URL: "https://attack.mitre.org/techniques/T1548/001/"}},
153 Standards: check.Standards{CWE: []string{"CWE-250"}, NIST: []string{"CM-7", "AC-6"}, CISTopic: "Filesystem: audit SUID and SGID executables"},
154 Run: func(ctx context.Context, s sys.System) []check.Result {
155 var bad []string
156 complete := true
157 for _, root := range []string{"/tmp", "/var/tmp", "/dev/shm", "/home", "/var/www", "/srv", "/opt", "/usr/local"} {
158 walk, err := s.Walk(ctx, root, 20000)
159 if err != nil || !walk.Complete {
160 complete = false
161 }
162 for _, f := range walk.Files {
163 if f.Mode&(fs.ModeSetuid|fs.ModeSetgid) != 0 {
164 bad = append(bad, perm(f.Mode)+" "+f.Path)
165 }
166 }
167 }
168 if len(bad) > 0 {
169 return []check.Result{check.FailR("filesystem", "SUID/SGID programs outside standard system paths", bad...)}
170 }
171 if !complete {
172 return []check.Result{check.IncompleteR("filesystem", "One or more filesystem walks were partial or unreadable")}
173 }
174 return []check.Result{check.PassR("filesystem", "No SUID/SGID programs in unusual locations")}
175 },
176 })
177
178 check.Register(&check.Check{
179 ID: "HOST-FS-005",
180 Title: "Cron Files Writable by Non-Root Users",
181 Domain: check.DomainHost,
182 Severity: check.High,
183 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
184 Summary: "Checks that system cron files and directories are owned by root and not writable by others.",
185 Rationale: "Cron runs these jobs as root. Anyone who can edit them can run any command as root at the next scheduled minute.",
186 Detection: "Stats /etc/crontab, /etc/cron.d and its files, and the cron.hourly/daily/weekly/monthly directories, flagging non-root owners or group/other write bits.",
187 Fix: "chown root:root <path> && chmod go-w <path> for each listed path (600 for /etc/crontab is a common hardening choice).",
188 Verify: "stat -c '%a %U %n' /etc/crontab /etc/cron.*",
189 Refs: []check.Ref{{Title: "crontab(5), Linux man-pages", URL: "https://man7.org/linux/man-pages/man5/crontab.5.html"}, {Title: "MITRE ATT&CK T1053.003, Cron", URL: "https://attack.mitre.org/techniques/T1053/003/"}},
190 Standards: check.Standards{CWE: []string{"CWE-732"}, NIST: []string{"AC-3", "CM-6"}, CISTopic: "Job schedulers: cron permissions"},
191 Run: func(_ context.Context, s sys.System) []check.Result {
192 paths := []string{"/etc/crontab", "/etc/cron.d", "/etc/cron.hourly", "/etc/cron.daily", "/etc/cron.weekly", "/etc/cron.monthly"}
193 more, _ := s.Glob("/etc/cron.d/*")
194 paths = append(paths, more...)
195 var bad []string
196 seen := 0
197 for _, p := range paths {
198 st, err := s.Stat(p)
199 if err != nil {
200 continue
201 }
202 seen++
203 if (st.UID > 0) || st.Mode.Perm()&0o022 != 0 {
204 bad = append(bad, fmt.Sprintf("%s uid=%d %s", perm(st.Mode), st.UID, p))
205 }
206 }
207 if seen == 0 {
208 return []check.Result{check.SkipR("cron", "No system cron files found")}
209 }
210 if len(bad) > 0 {
211 return []check.Result{check.FailR("cron", "Cron paths can be modified by non-root users", bad...)}
212 }
213 return []check.Result{check.PassR("cron", "Cron files are root-owned and not group/other writable")}
214 },
215 })
216}
217
218// worldWritableDirs lists directories under root, up to depth levels, that
219// are writable by others and lack the sticky bit. Stat does not follow
220// symlinks, so a symlink is never reported as a directory.
221func worldWritableDirs(s sys.System, root string, depth, limit int) []string {
222 var bad []string
223 pat := root
224 seen := 0
225 for d := 0; d < depth; d++ {
226 pat += "/*"
227 paths, _ := s.Glob(pat)
228 sort.Strings(paths)
229 for _, p := range paths {
230 if seen >= limit {
231 return bad
232 }
233 st, err := s.Stat(p)
234 if err != nil || !st.IsDir {
235 continue
236 }
237 seen++
238 if st.Mode.Perm()&0o002 != 0 && st.Mode&fs.ModeSticky == 0 {
239 bad = append(bad, perm(st.Mode)+" "+p+"/ (directory, no sticky bit)")
240 }
241 }
242 }
243 return bad
244}

Also available as plain Markdown for notes, scripts and AI tools. Same facts, same source, no styling.

Cite This Page

If this page helped your work, please cite it. The version and web address are filled in; the access date is today's date in your browser. Every other style, and when credit is required, is on How to Cite OpenVAPT.

APA 7
OpenVAPT. (2026). HOST-FS-005 Cron Files Writable by Non-Root Users (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/HOST-FS-005.html

In text: (OpenVAPT, 2026). Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.

BibTeX
@misc{openvapt_host_fs_005,
  author       = {{OpenVAPT}},
  title        = {{HOST-FS-005 Cron Files Writable by Non-Root Users}},
  year         = {2026},
  howpublished = {\url{https://openvapt.com/checks/HOST-FS-005.html}},
  note         = {OpenVAPT Check Catalogue, version 0.1.0-alpha.6. Accessed 16 October 2026}
}

Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.

Citing OpenVAPT does not mean OpenVAPT endorses, approves or has reviewed your work.