Skip to content

fix(localfile): reject symlinks in SetFileAttributes - #627

Open
sfc-gh-ikryvanos wants to merge 1 commit into
mainfrom
ikryvanos/localfile-symlink-setattr
Open

fix(localfile): reject symlinks in SetFileAttributes#627
sfc-gh-ikryvanos wants to merge 1 commit into
mainfrom
ikryvanos/localfile-symlink-setattr

Conversation

@sfc-gh-ikryvanos

@sfc-gh-ikryvanos sfc-gh-ikryvanos commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

%23%23 Summary

  • Reject SetFileAttributes on symlink paths (FailedPrecondition) so attribute changes cannot follow a link to an out-of-policy target
  • Use Lchown instead of Chown, and open immutable flag updates with O_NOFOLLOW on Linux as defense in depth
  • Add a regression test that a symlink target's mode is unchanged when attributes are requested via the link

%23%23 Test plan

  • go test ./services/localfile/server/...
  • GOOS=linux GOARCH=amd64 go build ./services/localfile/...
  • Confirm CI passes on the PR

Attribute changes followed symlinks, so chown/chmod/immutable
could escape path-based policy confinement. Lstat-reject
symlinks, use Lchown, and open immutable updates with O_NOFOLLOW.

Co-authored-by: Cursor <cursoragent@cursor.com>
// directory prefix could create a symlink inside that prefix pointing at an
// arbitrary target (e.g. /etc/shadow) and then chown/chmod/immutable the
// target, escaping the confinement. Lstat inspects the link itself.
if fi, err := os.Lstat(filename); err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix overall — switching chown to Lchown and opening the immutable path with O_NOFOLLOW makes those two operations safe at the syscall level, independent of any earlier check.

The chmod path is the exception. It still calls unix.Chmod(filename, ...), which follows symlinks, so its only protection is the os.Lstat guard at the top of the function. That leaves a time-of-check/time-of-use gap between the Lstat and this Chmod: a principal who can write into the confined directory can pass the check with a regular file and then swap it for a symlink to an out-of-policy target (e.g. /etc/shadow) before this line runs, and the chmod follows the link. That's the same link-following class the PR is closing, just narrowed to chmod and gated behind a race.

Linux doesn't give us a direct Lchmod, and fchmodat(AT_SYMLINK_NOFOLLOW) returns EOPNOTSUPP for chmod, so the race-free approach is the same one the immutable path already uses — open with O_NOFOLLOW and operate on the descriptor:

fd, err := os.OpenFile(filename, os.O_RDONLY|unix.O_NOFOLLOW, 0)
if err != nil {
    return nil, status.Errorf(codes.Internal, "error opening for chmod: %v", err)
}
defer fd.Close()
if err := unix.Fchmod(int(fd.Fd()), mode&modeMask); err != nil {
    return nil, status.Errorf(codes.Internal, "error from fchmod: %v", err)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants