path-traversal.test.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import { test, expect, describe } from "bun:test"
  2. import path from "path"
  3. import { Filesystem } from "../../src/util/filesystem"
  4. import { File } from "../../src/file"
  5. import { Instance } from "../../src/project/instance"
  6. import { tmpdir } from "../fixture/fixture"
  7. describe("Filesystem.contains", () => {
  8. test("allows paths within project", () => {
  9. expect(Filesystem.contains("/project", "/project/src")).toBe(true)
  10. expect(Filesystem.contains("/project", "/project/src/file.ts")).toBe(true)
  11. expect(Filesystem.contains("/project", "/project")).toBe(true)
  12. })
  13. test("blocks ../ traversal", () => {
  14. expect(Filesystem.contains("/project", "/project/../etc")).toBe(false)
  15. expect(Filesystem.contains("/project", "/project/src/../../etc")).toBe(false)
  16. expect(Filesystem.contains("/project", "/etc/passwd")).toBe(false)
  17. })
  18. test("blocks absolute paths outside project", () => {
  19. expect(Filesystem.contains("/project", "/etc/passwd")).toBe(false)
  20. expect(Filesystem.contains("/project", "/tmp/file")).toBe(false)
  21. expect(Filesystem.contains("/home/user/project", "/home/user/other")).toBe(false)
  22. })
  23. test("handles prefix collision edge cases", () => {
  24. expect(Filesystem.contains("/project", "/project-other/file")).toBe(false)
  25. expect(Filesystem.contains("/project", "/projectfile")).toBe(false)
  26. })
  27. })
  28. /*
  29. * Integration tests for File.read() and File.list() path traversal protection.
  30. *
  31. * These tests verify the HTTP API code path is protected. The HTTP endpoints
  32. * in server.ts (GET /file/content, GET /file) call File.read()/File.list()
  33. * directly - they do NOT go through ReadTool or the agent permission layer.
  34. *
  35. * This is a SEPARATE code path from ReadTool, which has its own checks.
  36. */
  37. describe("File.read path traversal protection", () => {
  38. test("rejects ../ traversal attempting to read /etc/passwd", async () => {
  39. await using tmp = await tmpdir({
  40. init: async (dir) => {
  41. await Bun.write(path.join(dir, "allowed.txt"), "allowed content")
  42. },
  43. })
  44. await Instance.provide({
  45. directory: tmp.path,
  46. fn: async () => {
  47. await expect(File.read("../../../etc/passwd")).rejects.toThrow("Access denied: path escapes project directory")
  48. },
  49. })
  50. })
  51. test("rejects deeply nested traversal", async () => {
  52. await using tmp = await tmpdir()
  53. await Instance.provide({
  54. directory: tmp.path,
  55. fn: async () => {
  56. await expect(File.read("src/nested/../../../../../../../etc/passwd")).rejects.toThrow(
  57. "Access denied: path escapes project directory",
  58. )
  59. },
  60. })
  61. })
  62. test("allows valid paths within project", async () => {
  63. await using tmp = await tmpdir({
  64. init: async (dir) => {
  65. await Bun.write(path.join(dir, "valid.txt"), "valid content")
  66. },
  67. })
  68. await Instance.provide({
  69. directory: tmp.path,
  70. fn: async () => {
  71. const result = await File.read("valid.txt")
  72. expect(result.content).toBe("valid content")
  73. },
  74. })
  75. })
  76. })
  77. describe("File.list path traversal protection", () => {
  78. test("rejects ../ traversal attempting to list /etc", async () => {
  79. await using tmp = await tmpdir()
  80. await Instance.provide({
  81. directory: tmp.path,
  82. fn: async () => {
  83. await expect(File.list("../../../etc")).rejects.toThrow("Access denied: path escapes project directory")
  84. },
  85. })
  86. })
  87. test("allows valid subdirectory listing", async () => {
  88. await using tmp = await tmpdir({
  89. init: async (dir) => {
  90. await Bun.write(path.join(dir, "subdir", "file.txt"), "content")
  91. },
  92. })
  93. await Instance.provide({
  94. directory: tmp.path,
  95. fn: async () => {
  96. const result = await File.list("subdir")
  97. expect(Array.isArray(result)).toBe(true)
  98. },
  99. })
  100. })
  101. })