Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zig-sftp

An SFTP (SSH File Transfer Protocol) server library and CLI in Zig.

Implements version 3 of the protocol (draft-ietf-secsh-filexfer-02), which is what OpenSSH speaks and therefore what interoperability means in practice.

Design

The protocol codec and the session logic know nothing about SSH. SFTP is length-prefixed packets over a reliable byte stream, so keeping the codec free of transport types means it can be driven from an SSH subsystem, a socket, or a test buffer, and tested directly rather than through a live connection.

  • protocol.zig - packet framing, wire types, encode/decode
  • server.zig - session state, handle table, path confinement

Usage

const sftp = @import("sftp");

var session = sftp.Session.init(allocator, "/srv/data");
defer session.deinit();

// Refuses anything that leaves the root.
const path = try session.resolve("docs/report.pdf");
defer allocator.free(path);

Parsing a stream:

while (try sftp.protocol.parse(buffer)) |packet| {
    // packet.body borrows from buffer
    buffer = buffer[packet.consumed..];
}

parse returns null when the buffer holds less than a whole packet. That is the normal case on a stream and is deliberately distinct from an error - a caller that treats "need more bytes" as a failure drops valid traffic.

CLI

zig-sftp version
zig-sftp check-path --root /srv/data docs/a.txt
zig-sftp serve --root /srv/data

serve speaks SFTP on stdin/stdout, which is how an SSH daemon invokes a subsystem:

Subsystem sftp /path/to/zig-sftp serve --root /srv/data

check-path is exposed as a command because path confinement is the part of a file server most worth being able to check from the outside:

$ zig-sftp check-path --root /srv/data ../etc/passwd
refused: ../etc/passwd (Escapes)

Path confinement

This is the whole security boundary, so it is worth stating what it refuses. Three distinct escapes are handled, all in resolveWithin:

  • .. traversal, including forms that only escape after normalisation
  • absolute paths - a leading / means the top of the served root, not the host's filesystem root, which is what SFTP clients intend
  • a sibling whose name merely shares the root's prefix (/srv/data-evil is not under /srv/data), which a naive startsWith check gets wrong

Normalisation is lexical and never touches the filesystem, because resolving against the filesystem would follow symlinks - and a symlink is exactly how a client escapes a root that only checks the textual path.

Status

Implemented and tested: the wire codec (framing, primitives, attributes, status replies), session and handle lifecycle, path confinement, error-to-status mapping, and version negotiation.

Not yet implemented: the filesystem request handlers (open/read/write/ readdir and friends) and the SSH transport. SFTP normally runs as an SSH subsystem, and hosting it means an SSH server - a separate concern this library deliberately does not take on. Point sshd at the CLI instead.

Testing

zig build test

Requires Zig 0.17.0-dev.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages