From e50e0bba5381a08e7d679a814f39a530a52c7870 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 24 Sep 2023 09:43:37 -0400 Subject: [PATCH] Allow file access not in Attachments table (#5876) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- package.json | 2 + plugins/storage/server/api/files.test.ts | 87 +++++++++++++++--- plugins/storage/server/api/files.ts | 76 +++++++++++---- .../storage/server/test/fixtures/avatar.jpg | Bin 0 -> 36231 bytes server/models/Attachment.test.ts | 10 -- server/models/Attachment.ts | 40 -------- server/models/helpers/AttachmentHelper.ts | 28 ++++++ server/storage/files/LocalStorage.ts | 16 ++-- yarn.lock | 7 +- 9 files changed, 181 insertions(+), 85 deletions(-) create mode 100644 plugins/storage/server/test/fixtures/avatar.jpg delete mode 100644 server/models/Attachment.test.ts diff --git a/package.json b/package.json index 8142b992e..8b3c7a6f1 100644 --- a/package.json +++ b/package.json @@ -198,6 +198,7 @@ "reflect-metadata": "^0.1.13", "refractor": "^3.6.0", "request-filtering-agent": "^1.1.2", + "resolve-path": "^1.4.0", "rfc6902": "^5.0.1", "sanitize-filename": "^1.6.3", "semver": "^7.5.2", @@ -291,6 +292,7 @@ "@types/readable-stream": "^4.0.2", "@types/redis-info": "^3.0.0", "@types/refractor": "^3.0.2", + "@types/resolve-path": "^1.4.0", "@types/semver": "^7.5.2", "@types/sequelize": "^4.28.10", "@types/slug": "^5.0.3", diff --git a/plugins/storage/server/api/files.test.ts b/plugins/storage/server/api/files.test.ts index 6683061d0..de7b5c877 100644 --- a/plugins/storage/server/api/files.test.ts +++ b/plugins/storage/server/api/files.test.ts @@ -1,9 +1,12 @@ -import { existsSync } from "fs"; +import { existsSync, copyFileSync } from "fs"; import { readFile } from "fs/promises"; import path from "path"; import FormData from "form-data"; +import { ensureDirSync } from "fs-extra"; +import { v4 as uuidV4 } from "uuid"; import env from "@server/env"; import "@server/test/env"; +import FileStorage from "@server/storage/files"; import { buildAttachment, buildUser } from "@server/test/factories"; import { getTestServer } from "@server/test/support"; @@ -18,11 +21,33 @@ describe("#files.create", () => { key: "public/foo/bar/baz.png", }, }); - const body = await res.json(); expect(res.status).toEqual(400); - expect(body.message).toEqual( - "key: Must be of the form uploads/// or public///" + }); + + it("should fail with status 404 if existing file is requested with key", async () => { + const user = await buildUser(); + const fileName = "images.docx"; + const key = path.join("uploads", user.id, uuidV4(), fileName); + + ensureDirSync( + path.dirname(path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key)) ); + + copyFileSync( + path.resolve(__dirname, "..", "test", "fixtures", fileName), + path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key) + ); + + const res = await server.get(`/api/files.get?key=${key}`); + expect(res.status).toEqual(404); + }); + + it("should fail with status 404 if non-existing file is requested with key", async () => { + const user = await buildUser(); + const fileName = "images.docx"; + const key = path.join("uploads", user.id, uuidV4(), fileName); + const res = await server.get(`/api/files.get?key=${key}`); + expect(res.status).toEqual(404); }); it("should succeed with status 200 ok and create a file", async () => { @@ -63,21 +88,17 @@ describe("#files.create", () => { describe("#files.get", () => { it("should fail with status 400 bad request if key is invalid", async () => { const res = await server.get(`/api/files.get?key=public/foo/bar/baz.png`); - const body = await res.json(); expect(res.status).toEqual(400); - expect(body.message).toEqual( - "key: Must be of the form uploads/// or public///" - ); }); - it("should fail with status 400 bad request if none of key or sig is supplied", async () => { + it("should fail with status 400 bad request if neither key or sig is supplied", async () => { const res = await server.get("/api/files.get"); const body = await res.json(); expect(res.status).toEqual(400); expect(body.message).toEqual("query: One of key or sig is required"); }); - it("should succeed with status 200 ok when file is requested using key", async () => { + it("should succeed with status 200 ok when attachment is requested using key", async () => { const user = await buildUser(); const fileName = "images.docx"; @@ -112,7 +133,7 @@ describe("#files.get", () => { ); }); - it("should succeed with status 200 ok when private file is requested using signature", async () => { + it("should succeed with status 200 ok when private attachment is requested using signature", async () => { const user = await buildUser(); const fileName = "images.docx"; @@ -147,4 +168,48 @@ describe("#files.get", () => { 'attachment; filename="images.docx"' ); }); + + it("should succeed with status 200 ok when file is requested using signature", async () => { + const user = await buildUser(); + const fileName = "images.docx"; + const key = path.join("uploads", user.id, uuidV4(), fileName); + const signedUrl = await FileStorage.getSignedUrl(key); + + ensureDirSync( + path.dirname(path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key)) + ); + + copyFileSync( + path.resolve(__dirname, "..", "test", "fixtures", fileName), + path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key) + ); + + const res = await server.get(signedUrl); + expect(res.status).toEqual(200); + expect(res.headers.get("Content-Type")).toEqual( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ); + expect(res.headers.get("Content-Disposition")).toEqual( + 'attachment; filename="images.docx"' + ); + }); + + it("should succeed with status 200 ok when avatar is requested using key", async () => { + const user = await buildUser(); + const key = path.join("avatars", user.id, uuidV4()); + + ensureDirSync( + path.dirname(path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key)) + ); + + copyFileSync( + path.resolve(__dirname, "..", "test", "fixtures", "avatar.jpg"), + path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key) + ); + + const res = await server.get(`/api/files.get?key=${key}`); + expect(res.status).toEqual(200); + expect(res.headers.get("Content-Type")).toEqual("application/octet-stream"); + expect(res.headers.get("Content-Disposition")).toEqual("attachment"); + }); }); diff --git a/plugins/storage/server/api/files.ts b/plugins/storage/server/api/files.ts index ef39a4ff1..de59497e3 100644 --- a/plugins/storage/server/api/files.ts +++ b/plugins/storage/server/api/files.ts @@ -1,14 +1,19 @@ +import JWT from "jsonwebtoken"; import Router from "koa-router"; +import mime from "mime-types"; import env from "@server/env"; -import { ValidationError } from "@server/errors"; +import { AuthenticationError, ValidationError } from "@server/errors"; import auth from "@server/middlewares/authentication"; import multipart from "@server/middlewares/multipart"; import { rateLimiter } from "@server/middlewares/rateLimiter"; import validate from "@server/middlewares/validate"; import { Attachment } from "@server/models"; +import AttachmentHelper from "@server/models/helpers/AttachmentHelper"; import { authorize } from "@server/policies"; +import FileStorage from "@server/storage/files"; import { APIContext } from "@server/types"; import { RateLimiterStrategy } from "@server/utils/RateLimiter"; +import { getJWTPayload } from "@server/utils/jwt"; import { createRootDirForLocalStorage } from "../utils"; import * as T from "./schema"; @@ -27,7 +32,10 @@ router.post( const { key } = ctx.input.body; const file = ctx.input.file; - const attachment = await Attachment.findByKey(key); + const attachment = await Attachment.findOne({ + where: { key }, + rejectOnEmpty: true, + }); if (attachment.isPrivate) { authorize(actor, "createAttachment", actor.team); @@ -46,26 +54,60 @@ router.get( auth({ optional: true }), validate(T.FilesGetSchema), async (ctx: APIContext) => { - const { key, sig } = ctx.input.query; const actor = ctx.state.auth.user; - let attachment: Attachment | null; + const key = getKeyFromContext(ctx); + const isSignedRequest = !!ctx.input.query.sig; + const { isPublicBucket, fileName } = AttachmentHelper.parseKey(key); + const skipAuthorize = isPublicBucket || isSignedRequest; + const cacheHeader = "max-age=604800, immutable"; - if (key) { - attachment = await Attachment.findByKey(key); - - if (attachment.isPrivate) { - authorize(actor, "read", attachment); - } - } else if (sig) { - attachment = await Attachment.findBySignature(sig); + if (skipAuthorize) { + ctx.set("Cache-Control", cacheHeader); + ctx.set( + "Content-Type", + (fileName ? mime.lookup(fileName) : undefined) || + "application/octet-stream" + ); + ctx.attachment(fileName); + ctx.body = FileStorage.getFileStream(key); } else { - throw ValidationError("Must provide either key or signature"); - } + const attachment = await Attachment.findOne({ + where: { key }, + rejectOnEmpty: true, + }); + authorize(actor, "read", attachment); - ctx.set("Content-Type", attachment.contentType); - ctx.attachment(attachment.name); - ctx.body = attachment.stream; + ctx.set("Cache-Control", cacheHeader); + ctx.set("Content-Type", attachment.contentType); + ctx.attachment(attachment.name); + ctx.body = attachment.stream; + } } ); +function getKeyFromContext(ctx: APIContext): string { + const { key, sig } = ctx.input.query; + if (sig) { + const payload = getJWTPayload(sig); + + if (payload.type !== "attachment") { + throw AuthenticationError("Invalid signature"); + } + + try { + JWT.verify(sig, env.SECRET_KEY); + } catch (err) { + throw AuthenticationError("Invalid signature"); + } + + return payload.key as string; + } + + if (key) { + return key; + } + + throw ValidationError("Must provide either key or sig parameter"); +} + export default router; diff --git a/plugins/storage/server/test/fixtures/avatar.jpg b/plugins/storage/server/test/fixtures/avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..856a55191f03d809d316051f4a935b6e27509205 GIT binary patch literal 36231 zcmb4KWmB9@uw7W(-DQErNpN>}cM@EJySw`Wi%W2q;2MHEA?PAO0s#^%xclY3Rrd$n z{yOvYGu2hy(|yjF*Ok{bfHVLJ0TBri0SO5a2^kp)1q}xc4HXrQ01F!fhnRqbgqVPc zh>Vh+nv9%|f`|yj2BKqNWM*L|rGCfpj){YwiJ9p?CveEf$Y>~N_-JVOOk_l4O#g3t z?FZnZz&XP?1L0@^@VIb5T)5X^0672vg!|6`|7Y+(1USIkN#7PlaRBh}a6lkD0s5BqSMlpA00a28 z6BmdJ5C@c-XUcXo)}X8pBRgQok7-`R+K%% z8j3v3!DjaPa!2Xq-L|*C!EN%SJEG4Qf_FT-HCdlX8(lG7Sql$J&#S0}*6T*F)ycVe z+OghAq6OlAp|sTvn#}t#r$y8>K{@d3S&&?wYMr7=!!Yt?Yk#}2EQ){!!J5~)vU57CF5H( z6gQd0(D(cIfM*x7YRkjF*QE9elIw=O_(8FK;5%YBQb>}+>?9C82DTgidipGVQ>ki{t8y<7UI_^ClktbF+wR?2jKY0i?)m$dnlsD~g+y_R(^I)UQRq@xdHbmz#CMzLn8V0t z=E4tXI1K4OLe@X=TR|s~yY3xm55WqlesrIdS&YszdKE`73K1UdV;5@BG^-Sb9NsVJ zug2C7eLkPqk!DdRYsD)@K`hj98V%0+al5DWJ#>5nD$4lrQfUJGjs)y)Es3y6$^dx< zm^x`LaDn?3h80vZGrVJvi7)w0mXFJ$>`D`d5o*+Chx1Gx)e}u&`J5&3Hf)z1y7aN& zP+8qn^?As$0L%eqPYrX$^-o2757D7^Z3@Iv=M(n1nH}DGlEPtRC`cfua-r+F-D|T8 zlK*{CtrzcSj25MW#=rLY^5N0m%TOl_M#|xo;Fh@P@d_w(l4#}(&HYqK`vV3Cz*9+N znLGyFU2YZzyBWa|)Cvy|rY{JEOCp$hNY5mHv9&xZ@q@M2=t`KqWNRCbgpGbRscCDD zqe139K?~xC7i~YvwhPy3e?PsTx%#0jLzE56+zaR0O;f$P`q()-_%m}gaDvalI+Wa9eqj-8c8>uag;rDz|s`c>WJ;|i6`KFWq{omg)=!GmJZ}Yx1b?k3LguvC4*I^&|59u znn{u;gor($OqWCn2>407y?jU;UD4a>p=w8lBJ34Vuigk$nsf%p75ObABfu}n>mOG! z(?O=W`A);NRPT_Ds_D@EqHfzr=WiX~4=TjpAVovUR%7fSia!#6rBCJ$ZZ8#I18P=$ zC+fGuU@h=V7*aD{&JTj9ND0z3APr<8T>4%MP`Gk|+)S>; zO5V*Olqt)!%c>!GS#~eb?(?pcHs^d|zE(?-?RzR4mcuQq`5{QWh8AP$8zKMo7i8V# z@wtCjp{7%^X%g|U-w$zm=@Ay~p8=w6kADR!J^GT#oDR*>+&+Ob%;hfqcJ@m3xD@L9 z5j7BBERcH~+DFi;t+)gQ#YAhBua)_5su)=%(C!cWdrRl98uCfs)Y9;mza@CQfJ$xCT{{c=kA$>v% z7S1V%wkes&?^q2@d64OD=C^g&%Bv}Woo9%T=j#W-&206$NSl*z!ZqENWBi*+SuB!= z{>d=PAMDDcoGFHUp+yOohRV6qcxVpYV}B|8BVIu&8rZkp@M z$0Ng^N`0sEV$eI%$)pc!>`2_%C+i8ltllh-{FV%RVT%UCDeBrF;CcdX8fN|GuoBKX zw214ozowMmq;;lECSNkHNri*5d&KXthtIO;1zmYge=lJlZEQ^%yG2c55O0TI?MqB= z)q&wE2<$Y^YG6Y8Xy;b^YVhk|<>{+K+zwW{0EWe+_mKLL*O#xLX60T*Or^#SP~<6%|q$Xp|Ze74C2P zpLwJ!5mh$$HOa$kTmQI0d@?}*6K0C8N7w^)KD7`ul@v7- zfydy6ve&D?vlmK5Piv6yO5Ky z_kr^cxp24=^OA9mx8@BG<~1Hrbq^l8hH0ssO#Y6wA%Y{R!=CT^zUZn#qZ*|#(~95a zs@P9mnY3Pci1V5~dp1s>R8tzKlgaK$&2!5;fgti|-=L>;7~At>%I}9) zck68tCmJ%NnMazEfXRQe00;IX%CQn4^7WrRWVK3?W~M%796aqq=ctLsVSbgUhAgG% z<=CoUK93KzQF$rV6jgw*oFL0n9$&a*_;NE3(J!k2MpxED1xtCY(hB%*5OWA${st!o=V;ZZfU4z0v_EMI=>Kdo`u5=CBTu?_Bl2BK+KIx zTb-B(-dciMQt!gZgW;L=qjxngcO>>DEjp2fT?&JM!xq_CnIGursX-7fK{^Xb20q4% zcF9{~xvy!rMD>v!3f}=S-;2(1r>OwL-oKfVAe>S9* zxwBT3aJ}1-9pTDKCI_?sHGRje_*UMSNK%g$E>Zt>Kc<{3l*OS+$emX71qoFj48})K zR@DRX{>d5)2x)d}6h$L%|JWHP!z@=ur;ls=0N=V%6pc!PafjB9D$f0A7i&vvByWZ* z3mM+Yy%o``IWQPvB~!liJ!CxMUlqDuFC zI<-~4>EV;gKDYpHXqsc-rf6%~p*4VCnZkAUJIhbt6A8tyRD{BV&@Dlv5#6x8HVqPy zgQg-3jhwRdInJgU%W;8$nYE;TdV7s@XUAh!y#uRqWcM64{T_Gr6Wah}86~covZ#Is z8&^1Ig`K026M(H1L^q*nh5R>05hr8Q_+BIB{fy8SuBDG^Q*$U#=}K}7r!>qPxvK|L zMyr&#^w4;vL+zwy%q?)Q>HhD`^RVx-G{$!l-F?lK)G{P92asVepecBG-!fB9gLIn( zRupO%2p5T# zP8@CiMT~*XJc@LEkYRGTp}Oh4$HVTj7pT;4?c;VslY$qz6}+}xNg1A_H~1CsOlfz}J0ccj6rxVGO<*d!oXm%b&w6oq~84XXOEmTgh4iLZ7gaI>uabtK;;a8xgo zo;;4X4wM;{xu!H5J(EB`wrJu6})_Hw)#7N`yig zpHO~g@}Lb<*qr%22ercEsBN3VVym_4BO_NUrjfiiiKPqNBs?nKk9yf?)8e-_=eNr) zQthQB?b?!87%W=t3-w!IV9hc`jOsn#Qw6m#R{aL6Rka{FO9Jk|*7y~tgiu9irvbkkDF3Tg;CTAmIjO;r%lrO6 z&3-RAZ)dvmPfw!#LWWG-7XOpT<+__%+$q@M$3zx`vqQU!E51pok|M6W_Pxt}80e{W zIwO}~7fkqxO$Sc`A-M7};K*O!fRx_atWKH82y80wjhd`Xbxx^#7o#5aYK5X*_&o5@ zyLKMaDczjiP+mq!%L z8D+Lb4-8HBD|>q-Ch1XJ&V(Czaz;G%G0*x>UB82klyQvyHg6tlhg~l5CG8RDR}Ge7 zU!^F>yW3)?%ti;>xRQKE9IhVufF9d#}w44EF)v`|}z^7Iq41urp2!ktI^RyxCLMoLe6l7}Cb z5h0rfJs)^8Y?h!%Zoc+)_K@sCdlx5xKJxmt37GsG9PBrO&ZnL|eOs zp#7@qS_;1{2pSdHIJD%{p=%EZZ2bzFl6UUp12AKDoq7Z0U&}g!aSz^$(B|#Gzvc50qf>r!Cjh?lWU^`K|ND5To3m}B$e73)2wK_-oD;G9fHaRu zN*YF_NW!}_owdW_X8*A+!38HV&3n;zck6QR!005$hCb)P#}&0_s%bHSKPjM)!k!Vt zQZsJ*8HaD@*L{QN-7;GknYj=r4*{Ms8pZjF--B(S1qmTLCjiKO70VR5p1``m7Y#7RuJIz$k3@!fBd?R=Mc0j?R64$}F@l&fi z(V4JnOT%qQxh-8vTO$kRJKf;c;2}x2xkjo&m70GN@9XrEd(PM15(Zm?!ursk#uBUK zQzkE}=UfJnoNpZlwLNe;?dkn6)#tNr4va0F71E|qUn+H~hm21t8HUD#X(~02NzQ%K zFRLdt5_}AEbdCcWcD84-m&UzD)MSy{a=f>w3rpFdkJ(Lk62;Jh#fM*2ro*7v5}Yz- zQ1qSQm<=O>RY>>q3e0V4Qj;ml{2ZQNlOk=`^6GZM@<@~( ze_Qv|t4>VRY<-%X>ezR|8Ltc@fS^cpx8rsQp5E^I2rfKyf=%+JhWF!v`FpNrNFF!5 zLDOyxV7@;&U#)$k$YXcxwvR@Usj!m7zS_DGR4hqMlV|X^Mg(Jvf852?dXbKEATsdH z;Z>>g;>;dTiOhgo(ZSu!U%9^_JvB1&607@8(Bwb+lfeYj=UB$$c8Bc1ngk@bRx^L@ zGjsn6@Q#y6;t?hY^R5|6+NBl{*Yeuyjc|E2 zBU<}`T8;AK_z}gQC=)jP@m^NJ6giQIdY>@I${5v{M3-IV_F*UTR80o+8xPVWeKvLT`83mYUUSct?#_VNvigKX?`Ns+9Sd(z zt=EM*f=+<^q}N+wC>=!~BoIv~QG{rde0p&UD~uWxGJs8w`~`)wGBIf^Fy?|kq4`Yp z3TU2q&Fs$X&cQBC)Iz@JKBPB6aO=`dIQ;sSmF|=>lhjp7EaXUE0o#H?1zdH1=CxT( zNW;p-ZyIhFiT*WlH${KbBgRy6ALQMR=5M#x#0q^)k<_|)I?jIK(;T7{7Er|L_tVN@ELv^O{v_0bk# zI{Hu8oH`rkaxc$ag<>#{bcb+va^S&xLqj}f9a2(jX&S&!%eH=!Aq?DqUh)c(vY`Aa z`&htE(*lC+@8R%&cqE?>Xt^oKs%{bf4#>ns+ zYnp!wQlso0Z5rT-j(jXy<^Mp2P_SG9F6qvmYioL6GH_RCK)j=amBK|ArxD8P!JBbY ze_M{5g)+ygOKvH@#%PT2odjzjfr*VTV7rw(w?1 zNYD$YD}-LI$)hTy_|GZCr2;?(A}Q)F;PZN@Vbstx7^&%DUPAPjD6~ZIP*a5{D(W|F~oPpV%GA!dKs&`XVoBm}{P{YL%1%xM0|l zcZ_5!5`)xqP7&VlsWj||>EwWrDU^i^DGr(oL0A5u`U*G%^uK{+BPpVBH*=X0{;3yS zu{Y7+P)zS>M0oZQA0bpBHxf$%Ogea-7K56Jk)WW?F$DjZ0z-mWRR15$MUv+q({e8% zWSv)k*z&43+pKU}3W#7QaP@~_6iIK3TV6;|`Tm??FliArD0sL9t)MWvNmism!~>Mj=S zZ@^4GW36FM{?F>9St|nMj??QNS+q7Ba9OE02d0m>`o*;h%fQDJA6!cZlRuYN)4~21 zR6c+2JtjL>87{{J9@h2t;;g;!i0Nn)YVLh^uI7<7NC|4HJ;|kkx?Z1do_Rfk6yxdp zG)bUpV=IdVE8wS@_AJ;0u(jNR&f5yE&e+4ULu7|>w9C)$>lfNSF>|W3znfP ztz;KN%rrR8U_=D$w?Zcpu>aG5 zSr|zBC-rQl$Ymw@rS~Cg7J=^|M>lW!{Wp(2SsV;Nt|8j{KRjK4kgeyQ>q3S&Be?4Q zE3|*wo?41HJd>lL#r>y74>SFJ=r~`!M0Hr2+ajYw*IqC-^Ona&ruka+52~vc)pUIB zH-ZZu9@qz80hFmh`TRNqhF@e7wlJ^@Sf1PRau>97{{EiGRz5f>R zJMa~te6!@4Qqm?DJ+O`w?y!;UQM#W}MHDGW{PiFDZcyOO;(!tew3z$Ouz`B7(8;18 z0D5=msgWAFs(`wgCs-4M_l}Bv zU><;Q1GX#*1RQn`(|^3uD*x(Cd||rbsvGVSH}hpVza?xAk@^wG;4rYKK3gVwOlMFW z9Y!p84(JnhZzKPDN)mqNu`hjc=Gyjx--SvdWs18n-u?)%h5r~Sk1(KL(Elq=yTSF8 zlru@$4ExCeTbes`!Hbm)UvFmfPni^6vg=NgH(U^Q0w0-)nE#BFE;Y6)*0#IKvVY+# zU<{E(&v%@t(vBhSdAdy;>tIT9#z<)c%kR9rTKtQ_sJQGR-)L|;z_(g_Pg~@JGOCbZ zo(o2$>%v+Yo)No|u6`n=hi4icVZ=EBfV{%~`KrnvlB9d0R_4sWJSBrs7ak_s%FStX zU4qB|DVa=g=Sfp13nG-A;etSZ?*ltp^qgx!wa6+vL^`A(hGGwkFBNDzj)yd{h%mq7ypr&@`I z%_uWBz%akyL>!Q92<*S$X{@Usl_0uDu<2>V=gd%u3&!!zq=;B;wb#pnRhVRg4ks3C zS@QH)KeW;cz%o8zI2zRpzTud#LCID+c$a$}GwMQWRe@_Zrj+W?nmB6U_-uvV_Okn; z?GCh@qtk-b~{v^dvrhSY;RfbqJO$5E*Eol7y-f*ENWa8M&SmC`vn<=PAV+YzCZCa zV`a84i0Sjmy4uworAaFvn9D00*x1E=jbk(I#@CY%W|gE<8^;;_gGgii+gD(_&X?MR zhp9)Vb`Q6@-@sss@v%_zjGYA>q-bZv zO^%>Dx`l~2&~!TT2q)c}H~;V_QAM^&ENx?%7ILYlzpvKLF%nt9Q5)c0`}c?6vWV|=#0uIFH4**h*E8uCW%IfakXT@{f{CwCUpG%F|e&x zh$&GgwiU&GUB~oj?fA69ivdVB2|qfil%b&mU7aEJx!uw|0$bZK5YkJm+d2d8?k#VY zS%tf!N7zWWpI<0a_NrA#_im zrSYz5 z#&+4N4(;<+!hLeX(Q)|)P^}l!!ML3z3woFi$!-1cD|eP`m=(Ezdq$ zUd{jPC`Bn_DZ`G7!rzW@_PYpbgSLLLTac^DZt1>e9GmPs^{;S0q^Pr$_4|W|BRgD) zh80}7w)p{Utgb_x{ab68pP=mg)Ss#VWyfp%h^@2EV6XplXRp6@;inQy-DPW{c&eC! zXhYaF45o`!GNZU4Wy_W(Fb>k_WnH(=xb8)6iv}SY!pG#zW~Iw*kEs=&ATzG{+{{x& zTCZpEpec;jKjvK?U(SYT+69;^(6ZsEgEM`gWE+Lxf?pyB)8G;;ZLVg;7ovJ-vYg07yfdhEFt>A|zdKwpAUZ z{RFmjiM9~ixWccy8u%;I&u4AlAn+{+>8n@R=$QoKhzEJd!v<@Q&?%zr9RqMjE3VTUxvJIF_D7(n9}o1( zvtz7Kg*2v3i$5J?=b_x7Q}vrVEcknKDQJ1VP*-V=U3`B|?oA?>Zn`=#Kna` zloRx$9ITy}zvZl;S{yc=4$$aFj|Os2KmgmBtRWq zVygz$guUO4QrS--9Evg}iU>`FRv#Errx%RCZkYLVjdd*;aHkir0130LwmXKZfWs4L zQPk(`XFVXe%e3egcFi(5kX;R{E0+#U#GJ5}Bn;VF@@!#qcuALBPerwUcP;k9g9%z! zl7kk&A+!}d?UzF5q*Wx4f3tQw0aGzQ9j#r@!z8q;RXn^L%dLC|G4fZC#k22kDQ5~i zsmfBBwOPlfq;)=0SW){=)DO0{L#$Feh(Ex^egI#fiZ?b?260+?X&*R4bQ;+dwKmm> z*PdbS+{N;44ir8_e}2ouks3yCSzB7+e-4Ze4nHMBwaVFg*HfwsPE0FMd?4%dIIiHa zU86F5aQozjwMz)T2)yv^W*AvK*c?PM9;(`3!ErSW^F#rAZ$D4;=>IS@i)l6-E1+eI zM`BB$!4j~;>(ISL-W0DdNJT(9{IgWot`qjRo4iEbnZ3OkWrNOA0lpEzKO)Tc*+s<~ zZMyj(4)4qW3C(wJ_2}1g+kF-^VQjo$)XQb+qq6bs0AVSj4(;G3HoKS6Pg&3zNoK4<+ z|LOF&K9neRaR3hwEi8Adkoa`ERuNMr&V*|K3&t}^&j&}@Fp_ZlaM&EgU@>l4_6pFz z?J|0LB;)_v&|5CC7pS8ZRx|{p5)?^ZkU3`KpjANI#cf5ROm=xIPTCav>p5sYvAp#1 zO)EL&2-Sy&+jf#2u|$*e!Liw4Fpx5Gr~>smC4RA-q9kV`0Aghc0RHN_tvRh!6ik)J zz=*hrvhEkXT-mw0T~w1DH=Zc9)^18M&_awVE$}@2a?{rmFY)dF7! zMMl@65uhCVzV;ZfKjCM~;3jKuZb10reLG8@*xlI{+jQqX<&ZZac>n44W+BW>!Acxc zt6mk0eD70o5F4VdhT~iQ23Eb*^JSSeoft02j|jh}i+{nvL2W|Nb+E;77Q)|=Jh~rA z&u=0+-Pvn7+B#G{@SL_UNnZw*XwNkZ-x62=y&p4qEnf?QR{AX(hah^@*ZxfYJ~@cx zV{IB7Gvlvd67BQ%IYQOD=8&>*tevr;kJzWj4Vv4LQv89qz_U#94%{stf#r#idDB?W z?=TS+IIf=t6MU!w$W1@ajqjHi3RSIb{KypD%%cH<58k|2ODg9Tex~ekJG=5#!?y9j zH?&S1)!qHq;295;7FQFMpF$ImCKvX%x8m-b?JDmf(m|OS7C?fZI%UB;8s&Hq43XGv zD|8`L0*-9AZB~VIIlJox$fr z;dJ7uN-8U9GJv-Vb~A0ti8u&N3H&gWO3(!7y#jxtfXh9UyIBV5{PO^ixr zU5yno;ofOoK5e{&aS3KChh7?QVD7AJsBCK8vv6kTXLx=*ed3cq-+7sdo|saGZAZCUfffD zVZcN6wRz_yU06}$GNR!mN2x?_NR=BdKYs9T>FOVxB}gRm>hP5G3u_|XjV$Swo;f~` zTwiudNPAp?TwD}t`k5q=SWnwrpQ@m018K8fwq{>_v;x{(WcK}6M9G&JU49Dn3$?k* zDkDbDai)}A)7j*s3eSsd{|t;!rS(nr%cb$@>XsUHUG>Y#g2j*fALh}?g-8ipxBiN| z1igc`w7B|@$O)GsD)5yRs6*g|I^Xl7^GbI#-Oi1HoNJ%WNS1+po%fVhBVWC3Ai!ur zpx|YxerKhwI7gDAg%+yR@GN3LfCH3%Wg5{*kg?-tfnTP|hZHTs++g;B(gMW_Oq}Lq zEk7()X)5*c%03E~zqq!oh4jnEU3|x&-;lna*8sft!|vH==w8Tdo@~H!G+dxOBz6#{ zPys)(zpmRnu27tzbPSGffs7eVMj4QU6Ds{L$j-&tL3j`u9`tFHiOUflSsHLaXa2l4d>{z;_^ zh#hFL@P7T%^5C1%oLN;0c^}m~PvPXD^YuvR-UvpbnLDyjM!CYAiXw>(Lj4?6o;vq@ z3r(B`qEEbS>>xA8Y`a5eZh5YC0}L~}yVuy$?QFvrA(#vK*KE>J#mSNsb9VPrKy!X0 zuV%v#!4{l#PI#dE_er?acH)itRe{s>($`Hk6TJp{QYwZDPfJzvslUkm*Cl;m2 z6;iJOhHR6=Xbcm(;VvES0bxeSq(~zAFlG%_j!dQ(_88~+9OUND#xGBnQd6NMWn80? z32;MttP$~he=(1Qs{Hp>twm)L%-i9I6E*mClf=pn6q+Z?tv_TqPi#NGR2=Q{Zik3( zzXFKaXYf^y&Pv0AOZ%b^cHb(|L$0ebU@KfUz-Q*@xI`^g63+MMfMnO?L?_oQe@C1C zadn|J;GDzL0!~|ZCZzAXPz9-WNHwyCmhN3ga#X|2(O6&mw>wx1>ldl83U3^i90Md4 z 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 QCj#@T-Ax$(0B-01*@JRrKmY&$ literal 0 HcmV?d00001 diff --git a/server/models/Attachment.test.ts b/server/models/Attachment.test.ts deleted file mode 100644 index b220e705f..000000000 --- a/server/models/Attachment.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { buildAttachment } from "@server/test/factories"; -import Attachment from "./Attachment"; - -describe("#findByKey", () => { - it("should return the correct attachment given a key", async () => { - const attachment = await buildAttachment(); - const found = await Attachment.findByKey(attachment.key); - expect(found?.id).toBe(attachment.id); - }); -}); diff --git a/server/models/Attachment.ts b/server/models/Attachment.ts index 49f1c2a2b..25e37eb08 100644 --- a/server/models/Attachment.ts +++ b/server/models/Attachment.ts @@ -1,7 +1,6 @@ import { createReadStream } from "fs"; import path from "path"; import { File } from "formidable"; -import JWT from "jsonwebtoken"; import { QueryTypes } from "sequelize"; import { BeforeDestroy, @@ -15,10 +14,7 @@ import { IsNumeric, BeforeUpdate, } from "sequelize-typescript"; -import env from "@server/env"; -import { AuthenticationError } from "@server/errors"; import FileStorage from "@server/storage/files"; -import { getJWTPayload } from "@server/utils/jwt"; import { ValidateKey } from "@server/validation"; import Document from "./Document"; import Team from "./Team"; @@ -172,42 +168,6 @@ class Attachment extends IdModel { return parseInt(result?.[0]?.total ?? "0", 10); } - /** - * Find an attachment given a JWT signature. - * - * @param sign - The signature that uniquely identifies an attachment - * @returns A promise resolving to attachment corresponding to the signature - * @throws {AuthenticationError} Invalid signature if the signature verification fails - */ - static async findBySignature(sign: string): Promise { - const payload = getJWTPayload(sign); - - if (payload.type !== "attachment") { - throw AuthenticationError("Invalid signature"); - } - - try { - JWT.verify(sign, env.SECRET_KEY); - } catch (err) { - throw AuthenticationError("Invalid signature"); - } - - return this.findByKey(payload.key); - } - - /** - * Find an attachment given a key - * - * @param key The key representing attachment file path - * @returns A promise resolving to attachment corresponding to the key - */ - static async findByKey(key: string): Promise { - return this.findOne({ - where: { key }, - rejectOnEmpty: true, - }); - } - // associations @BelongsTo(() => Team, "teamId") diff --git a/server/models/helpers/AttachmentHelper.ts b/server/models/helpers/AttachmentHelper.ts index 1f69e57a2..bad0890d0 100644 --- a/server/models/helpers/AttachmentHelper.ts +++ b/server/models/helpers/AttachmentHelper.ts @@ -33,6 +33,34 @@ export default class AttachmentHelper { return `${keyPrefix}/${name}`; } + /** + * Parse a key into its constituent parts + * + * @param key The key to parse + * @returns The constituent parts + */ + static parseKey(key: string): { + bucket: string; + userId: string; + id: string; + fileName: string | undefined; + isPublicBucket: boolean; + } { + const parts = key.split("/"); + const bucket = parts[0]; + const userId = parts[1]; + const id = parts[2]; + const [fileName] = parts.length > 3 ? parts.slice(-1) : []; + + return { + bucket, + userId, + id, + fileName, + isPublicBucket: bucket === Buckets.avatars || bucket === Buckets.public, + }; + } + /** * Get the ACL to use for a given attachment preset * diff --git a/server/storage/files/LocalStorage.ts b/server/storage/files/LocalStorage.ts index 93a5e8ac9..92edca3d6 100644 --- a/server/storage/files/LocalStorage.ts +++ b/server/storage/files/LocalStorage.ts @@ -12,6 +12,7 @@ import path from "path"; import { Readable } from "stream"; import invariant from "invariant"; import JWT from "jsonwebtoken"; +import safeResolvePath from "resolve-path"; import env from "@server/env"; import Logger from "@server/logging/Logger"; import BaseStorage from "./BaseStorage"; @@ -68,11 +69,11 @@ export default class LocalStorage extends BaseStorage { src = Readable.from(body); } - const destPath = path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key); - closeSync(openSync(destPath, "w")); + const filePath = this.getFilePath(key); + closeSync(openSync(filePath, "w")); return new Promise((resolve, reject) => { - const dest = createWriteStream(destPath) + const dest = createWriteStream(filePath) .on("error", reject) .on("finish", () => resolve(this.getUrlForKey(key))); @@ -86,7 +87,7 @@ export default class LocalStorage extends BaseStorage { }; public async deleteFile(key: string) { - const filePath = path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key); + const filePath = this.getFilePath(key); try { await unlink(filePath); } catch (err) { @@ -112,12 +113,15 @@ export default class LocalStorage extends BaseStorage { }; public getFileStream(key: string) { + return createReadStream(this.getFilePath(key)); + } + + private getFilePath(key: string) { invariant( env.FILE_STORAGE_LOCAL_ROOT_DIR, "FILE_STORAGE_LOCAL_ROOT_DIR is required" ); - const filePath = path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key); - return createReadStream(filePath); + return safeResolvePath(env.FILE_STORAGE_LOCAL_ROOT_DIR, key); } } diff --git a/yarn.lock b/yarn.lock index 589259c78..87b4f3673 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3436,6 +3436,11 @@ dependencies: "@types/prismjs" "*" +"@types/resolve-path@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@types/resolve-path/-/resolve-path-1.4.0.tgz#7e5abe36da0ba037e9d84348bef9e85db752cae4" + integrity sha512-Zh5blp0SEy8yHxnN3yOnInh9NtZHze1c6sl/Neg2or/MbEEoxg69qZScrSqe4BYIRQD722r/ouV822Av1mKwRA== + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -11605,7 +11610,7 @@ resolve-options@^1.1.0: resolve-path@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7" - integrity sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc= + integrity sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w== dependencies: http-errors "~1.6.2" path-is-absolute "1.0.1"