StaticServerForbidSymlinks.test.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import { vi, describe, beforeEach, it } from 'vitest'
  2. import path from 'node:path'
  3. import sinon from 'sinon'
  4. const modulePath = path.join(
  5. import.meta.dirname,
  6. '../../../app/js/StaticServerForbidSymlinks'
  7. )
  8. describe('StaticServerForbidSymlinks', function () {
  9. beforeEach(async function (ctx) {
  10. ctx.settings = {
  11. path: {
  12. compilesDir: '/compiles/here',
  13. },
  14. }
  15. ctx.fs = {}
  16. vi.doMock('@overleaf/settings', () => ({
  17. default: ctx.settings,
  18. }))
  19. vi.doMock('fs', () => ({
  20. default: ctx.fs,
  21. }))
  22. ctx.ForbidSymlinks = (await import(modulePath)).default
  23. ctx.dummyStatic = (rootDir, options) => (req, res, next) =>
  24. // console.log "dummyStatic serving file", rootDir, "called with", req.url
  25. // serve it
  26. next()
  27. ctx.StaticServerForbidSymlinks = ctx.ForbidSymlinks(
  28. ctx.dummyStatic,
  29. ctx.settings.path.compilesDir
  30. )
  31. ctx.req = {
  32. params: {
  33. project_id: '12345',
  34. },
  35. }
  36. ctx.res = {}
  37. ctx.req.url = '/12345/output.pdf'
  38. })
  39. describe('sending a normal file through', function () {
  40. beforeEach(function (ctx) {
  41. ctx.fs.realpath = sinon
  42. .stub()
  43. .callsArgWith(
  44. 1,
  45. null,
  46. `${ctx.settings.path.compilesDir}/${ctx.req.params.project_id}/output.pdf`
  47. )
  48. })
  49. it('should call next', async function (ctx) {
  50. await new Promise((resolve, reject) => {
  51. ctx.res.sendStatus = function (resCode) {
  52. resCode.should.equal(200)
  53. resolve()
  54. }
  55. ctx.StaticServerForbidSymlinks(ctx.req, ctx.res, err => {
  56. if (err) reject(err)
  57. resolve()
  58. })
  59. })
  60. })
  61. })
  62. describe('with a missing file', function () {
  63. beforeEach(function (ctx) {
  64. ctx.fs.realpath = sinon
  65. .stub()
  66. .callsArgWith(
  67. 1,
  68. { code: 'ENOENT' },
  69. `${ctx.settings.path.compilesDir}/${ctx.req.params.project_id}/unknown.pdf`
  70. )
  71. })
  72. it('should send a 404', async function (ctx) {
  73. await new Promise((resolve, reject) => {
  74. ctx.res.sendStatus = function (resCode) {
  75. resCode.should.equal(404)
  76. resolve()
  77. }
  78. ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
  79. })
  80. })
  81. })
  82. describe('with a new line', function () {
  83. beforeEach(function (ctx) {
  84. ctx.req.url = '/12345/output.pdf\nother file'
  85. ctx.fs.realpath = sinon.stub().yields()
  86. })
  87. it('should process the correct file', async function (ctx) {
  88. await new Promise((resolve, reject) => {
  89. ctx.res.sendStatus = () => {
  90. ctx.fs.realpath.should.have.been.calledWith(
  91. `${ctx.settings.path.compilesDir}/12345/output.pdf\nother file`
  92. )
  93. resolve()
  94. }
  95. ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
  96. })
  97. })
  98. })
  99. describe('with a symlink file', function () {
  100. beforeEach(function (ctx) {
  101. ctx.fs.realpath = sinon
  102. .stub()
  103. .callsArgWith(1, null, `/etc/${ctx.req.params.project_id}/output.pdf`)
  104. })
  105. it('should send a 404', async function (ctx) {
  106. await new Promise((resolve, reject) => {
  107. ctx.res.sendStatus = function (resCode) {
  108. resCode.should.equal(404)
  109. resolve()
  110. }
  111. ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
  112. })
  113. })
  114. })
  115. describe('with a relative file', function () {
  116. beforeEach(function (ctx) {
  117. ctx.req.url = '/12345/../67890/output.pdf'
  118. })
  119. it('should send a 404', async function (ctx) {
  120. await new Promise((resolve, reject) => {
  121. ctx.res.sendStatus = function (resCode) {
  122. resCode.should.equal(404)
  123. resolve()
  124. }
  125. ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
  126. })
  127. })
  128. })
  129. describe('with a unnormalized file containing .', function () {
  130. beforeEach(function (ctx) {
  131. ctx.req.url = '/12345/foo/./output.pdf'
  132. })
  133. it('should send a 404', async function (ctx) {
  134. await new Promise((resolve, reject) => {
  135. ctx.res.sendStatus = function (resCode) {
  136. resCode.should.equal(404)
  137. resolve()
  138. }
  139. ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
  140. })
  141. })
  142. })
  143. describe('with a file containing an empty path', function () {
  144. beforeEach(function (ctx) {
  145. ctx.req.url = '/12345/foo//output.pdf'
  146. })
  147. it('should send a 404', async function (ctx) {
  148. await new Promise((resolve, reject) => {
  149. ctx.res.sendStatus = function (resCode) {
  150. resCode.should.equal(404)
  151. resolve()
  152. }
  153. ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
  154. })
  155. })
  156. })
  157. describe('with a non-project file', function () {
  158. beforeEach(function (ctx) {
  159. ctx.req.url = '/.foo/output.pdf'
  160. })
  161. it('should send a 404', async function (ctx) {
  162. await new Promise((resolve, reject) => {
  163. ctx.res.sendStatus = function (resCode) {
  164. resCode.should.equal(404)
  165. resolve()
  166. }
  167. ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
  168. })
  169. })
  170. })
  171. describe('with a file outside the compiledir', function () {
  172. beforeEach(function (ctx) {
  173. ctx.req.url = '/../bar/output.pdf'
  174. })
  175. it('should send a 404', async function (ctx) {
  176. await new Promise((resolve, reject) => {
  177. ctx.res.sendStatus = function (resCode) {
  178. resCode.should.equal(404)
  179. resolve()
  180. }
  181. ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
  182. })
  183. })
  184. })
  185. describe('with a file with no leading /', function () {
  186. beforeEach(function (ctx) {
  187. ctx.req.url = './../bar/output.pdf'
  188. })
  189. it('should send a 404', async function (ctx) {
  190. await new Promise((resolve, reject) => {
  191. ctx.res.sendStatus = function (resCode) {
  192. resCode.should.equal(404)
  193. resolve()
  194. }
  195. ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
  196. })
  197. })
  198. })
  199. describe('with a github style path', function () {
  200. beforeEach(function (ctx) {
  201. ctx.req.url = '/henryoswald-latex_example/output/output.log'
  202. ctx.fs.realpath = sinon
  203. .stub()
  204. .callsArgWith(
  205. 1,
  206. null,
  207. `${ctx.settings.path.compilesDir}/henryoswald-latex_example/output/output.log`
  208. )
  209. })
  210. it('should call next', async function (ctx) {
  211. await new Promise((resolve, reject) => {
  212. ctx.res.sendStatus = function (resCode) {
  213. resCode.should.equal(200)
  214. resolve()
  215. }
  216. ctx.StaticServerForbidSymlinks(ctx.req, ctx.res, err => {
  217. if (err) reject(err)
  218. resolve()
  219. })
  220. })
  221. })
  222. })
  223. describe('with an error from fs.realpath', function () {
  224. beforeEach(function (ctx) {
  225. ctx.fs.realpath = sinon.stub().callsArgWith(1, 'error')
  226. })
  227. it('should send a 500', async function (ctx) {
  228. await new Promise((resolve, reject) => {
  229. ctx.res.sendStatus = function (resCode) {
  230. resCode.should.equal(500)
  231. resolve()
  232. }
  233. ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
  234. })
  235. })
  236. })
  237. })