╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ serve_ssh.zig ────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
262 263 fn isSubPath(parent: []const u8, child: []const u8) bool { 264 if (std.mem.eql(u8, parent, std.fs.path.sep_str)) return std.fs.path.isAbsolute(child); 265 if (!std.mem.startsWith(u8, child, parent)) return false; 266 return child.len == parent.len or child[parent.len] == std.fs.path.sep; 267 } 268 269 fn writeError(sess: *ssh.SessionCtx, msg: []const u8) !void { 270 var buf: [256]u8 = undefined; 271 const text = try std.fmt.bufPrint(&buf, "haxy ssh: {s}\n", .{msg}); 272 try sess.writeBytes(text); 273 } 274 275 test "parseGitCommand rejects malformed input" { 276 const allocator = std.testing.allocator; 277 try std.testing.expectError(error.InvalidCommand, parseGitCommand(allocator, "git-upload-pack")); 278 try std.testing.expectError(error.UnsupportedService, parseGitCommand(allocator, "ls -la")); 279 try std.testing.expectError(error.UnsupportedService, parseGitCommand(allocator, "git-fake-pack 'repo'")); 280 } 281 282 test "parseGitCommand happy paths" { 283 const allocator = std.testing.allocator; 284 285 { 286 const parsed = try parseGitCommand(allocator, "git-upload-pack 'some-repo'"); 287 defer parsed.deinit(allocator); 288 try std.testing.expectEqual(GitService.upload_pack, parsed.service); 289 try std.testing.expectEqualStrings("some-repo", parsed.dir); 290 } 291 { 292 const parsed = try parseGitCommand(allocator, "git-receive-pack 'user/proj'"); 293 defer parsed.deinit(allocator); 294 try std.testing.expectEqual(GitService.receive_pack, parsed.service); 295 try std.testing.expectEqualStrings("user/proj", parsed.dir); 296 } 297 { 298 const parsed = try parseGitCommand(allocator, "git-upload-pack repo"); 299 defer parsed.deinit(allocator); 300 try std.testing.expectEqual(GitService.upload_pack, parsed.service); 301 try std.testing.expectEqualStrings("repo", parsed.dir); 302 } 303 } 304 305 test "isSubPath rejects prefix collisions and traversal" { 306 const sep = std.fs.path.sep_str; 307 const parent = sep ++ "srv" ++ sep ++ "git"; 308 309 try std.testing.expect(isSubPath(parent, parent ++ sep ++ "repo")); 310 try std.testing.expect(isSubPath(parent, parent)); 311 312 // common-prefix-but-not-subpath: "/srv/git" should NOT swallow 313 // "/srv/git2" or "/srv/gitignore" just because the bytes start the same. 314 try std.testing.expect(!isSubPath(parent, parent ++ "2")); 315 try std.testing.expect(!isSubPath(parent, parent ++ "ignore")); 316 317 // resolveRepoPath normalizes `..` first, so `/srv/git/../etc/passwd` 318 // becomes `/etc/passwd` — isSubPath then rejects it. 319 try std.testing.expect(!isSubPath(parent, sep ++ "etc" ++ sep ++ "passwd")); 320 321 const builtin = @import("builtin"); 322 323 if (.windows != builtin.os.tag) { 324 try std.testing.expect(isSubPath(sep, sep ++ "anything")); 325 try std.testing.expect(isSubPath(sep, sep)); 326 } 327 } 328