fix: resolve P2-level issues found in code review (28 fixes across pkg/server/internal/edge/drivers) - #85
Open
Mcchen1008 wants to merge 6 commits into
Open
fix: resolve P2-level issues found in code review (28 fixes across pkg/server/internal/edge/drivers)#85Mcchen1008 wants to merge 6 commits into
Mcchen1008 wants to merge 6 commits into
Conversation
- http.ts buildUrl: append query params before the URL fragment instead of into it (params after '#' were never sent to the server) - http.ts fetchWithTimeout: honor caller-provided AbortSignal via AbortSignal.any instead of unconditionally overwriting it; thread config.signal through get/post/request - http.ts isSafeUrl: block *.nip.io and *.sslip.io entirely - only 127.0.0.1.nip.io was blocked, so <any-ip>.nip.io bypassed SSRF checks - stream.ts parseRange: reject NaN end, support RFC 7233 suffix ranges (bytes=-N) and clamp oversized end instead of dropping the request - csrf.ts getCookie: never call decodeURIComponent(undefined) (returned the string "undefined") and fall back to the raw value on invalid percent-encoding instead of throwing URIError (500) - totp.ts verifyBackupCode: strip hyphens on both sides - generated backup codes are XXXX-XXXX, so user input without the hyphen never matched - utils.ts formatBytes: clamp unit index for >= 1024PB and return "0 Bytes" for non-finite/negative input instead of "NaN undefined" - validators.ts: align role schema with the UserRole enum (0=GENERAL, 1=GUEST, 2=ADMIN); the previous comment/default were inverted, which would default-create a GUEST and map "2" to ADMIN - xml.ts generateWebDavXml: XML-escape hrefs - directory names with & < > produced invalid PROPFIND multistatus (and allowed stored XML injection via crafted directory names)
- admin.ts: return a real HTTP 401 for unauthorized /api/admin/* requests;
c.json({code:401}) without the status arg returned HTTP 200
- auth.ts /login and /login/hash: record login failures + audit log on OTP
failure too - previously a correct password allowed unlimited TOTP
guessing, bypassing the per-IP/account lockout
- user.ts updatePwdHandler: reject revoked tokens (jti in logout blacklist)
and disabled accounts, aligning with authUserFromReq semantics
- sso.ts postMessageHtml: escape backslash, quotes and line terminators -
only escaping <> let a value containing '"' break out of the JS string
literal in the inline postMessage script (XSS via IdP-provided fields)
- sso.ts autoRegister: persist with the caller's c.env instead of the
non-existent db.env property (only worked via a saveDb internal fallback)
- fs.ts /fs/get share branch: URL-encode shareId and sub-path in raw_url; file names containing ? # % truncated the URL or broke requests - fs.ts add_offline_download: return HTTP 400 (was body code 400 with HTTP 200) - fs.ts: decode File-Path/Upload-Path headers via a safe helper and return 400 on malformed percent-encoding instead of an uncaught URIError -> 500 (4 call sites, aligned with raw.ts behavior) - fs.ts fetchArchiveBytes: pass the base_path-resolved actual path to driver.get(), matching the getItem//fs/link convention; the raw virtual path resolved archives to the wrong object for base_path users - webdav.ts PROPFIND: honor the Depth header - Depth: 0 now returns only the collection itself per RFC 4918 (previously depth was read but both branches returned the full child list) - assets.ts resolveCdnBase: read the version setting from db.settings (TS getDb() has no Go-style db.get(sql) API); the old call always threw a swallowed TypeError so $version was stuck at 'latest'
- internal/stream parseRangeHeader: return null for malformed/multi-range/ unsatisfiable Range instead of NaN garbage; support RFC 7233 suffix ranges (bytes=-N) and clamp end to size-1. Previously 'Range: bytes=-500' produced start=NaN and crashed fs.createReadStream with ERR_OUT_OF_RANGE (500) on /d downloads - raw.ts: fall back to a full-content 200 when the Range header does not parse (RFC allows ignoring Range) instead of streaming an invalid slice - proxy_request.ts exceedsProxyPayloadLimit: treat unparseable Range as full-size transfer for the payload-limit check - archive/zip.ts extractZipEntry: await writer.write()/close() so inflate errors propagate through the await chain instead of becoming unhandled rejections with truncated output - upload/multipart.ts snapshot: sum actual per-chunk bytes - the last chunk is smaller than chunk_size when size is not a multiple, so received.size * chunk_size over-counted (e.g. 15MB file / 10MB chunks reported 20MB)
- esa-entry.ts: cache the KV probe result for 60s (module level) instead of burning one KV subrequest per request - ESA allows only 8 per request; keep probing the raw EdgeKV so failures stay visible - esa-entry.ts delete: rethrow on failure (consistent with put) and only invalidate caches after success - swallowing errors made callers believe a delete succeeded while the value would reappear on next get - middleware.js: add /dav and /s3 to the backend path whitelist; both are mounted routes, and html-accepting GETs to them were rewritten to the SPA index.html instead of hitting the backend - loadEnv.js: strip paired quotes and CRLF remnants from .env values so JWT_SECRET="abc" no longer yields an 8-char quoted secret
- webdav/util.ts: unescape XML entities in displayname so file names like 'a&b.txt' are listed correctly (previously broke follow-up rename/move/get); self-detection now strips the scheme://host prefix and uses exact matching - the old bare endsWith() matched any entry when the target was the root and swallowed same-named subdirectories - s3/util.ts: unescape ETag entities before stripping quotes (ETag is serialized as "hex" in ListObjects XML); ListObjectsV1 fallback marker now converts the last entry via getKey() - the raw virtual path lacked the trailing slash for folders and restarted the same page, duplicating entries or looping
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
P2 review fixes: 24 issues across pkg / server / internal / edge / drivers
Following a full code review of the repository, this PR fixes the P2-level issues found (edge-condition bugs, encoding errors, status-code mistakes and latent defects). Every item below was manually verified against the source before fixing.
pkg (
src/backend/pkg/)http.tsbuildUrl#, landing in the fragment and never reaching the serverhttp.tsfetchWithTimeoutAbortSignaldeclared inFetchConfigbut unconditionally overwritten by the timeout signalAbortSignal.any(guarded for old runtimes); threadsignalthroughget/post/requesthttp.tsisSafeUrl127.0.0.1.nip.iowas blocked;<any-ip>.nip.io(and all ofsslip.io) bypassed SSRF protection*.nip.io/*.sslip.io/*.xip.ioentirely (adminallowHostswhitelist still takes precedence)stream.tsparseRangebytes=5-x->end=NaNescaped all checks and returned an invalid slice; suffix ranges (bytes=-N) unsupportedisNaNguards, suffix-range support, end clamped tototal-1per RFC 7233csrf.tsgetCookiedecodeURIComponent(undefined)returned the string"undefined"; invalid%sequences threwURIError-> 500indexOf("=")parsing + try/catch fallback to raw valuetotp.tsverifyBackupCodeXXXX-XXXX, but normalization only stripped whitespace - input without the hyphen could never match[-\s]on both sidesutils.tsformatBytes>= 1024PBreturned"1024 undefined"; non-finite/negative input returned"NaN undefined""0 Bytes"for invalid inputvalidators.tsroleschema comment/default inverted vs.UserRoleenum (default 1 = GUEST; "2" mapped to ADMIN)0=GENERAL, 1=GUEST, 2=ADMIN, default 0xml.tsgenerateWebDavXmlpathinserted raw into<d:href>- names with& < >produced invalid PROPFIND XML (stored XML injection possible)server (
src/backend/server/)admin.ts/api/admin/*returned HTTP 200 (401 only in the body)c.json(..., 401)auth.ts/login,/login/hashrecordLoginFailure- with a correct password, 6-digit TOTP could be brute-forced indefinitely, bypassing lockoutuser.tsupdatePwdHandlerdisabledaccounts were ignoredsso.tspostMessageHtml< >escaped; a"in the IdP-provided value broke out of the JS string literal in the inline script (XSS)\,",<>, CR/LF/U+2028/9sso.tsautoRegistersaveDb(db, db.env)-db.envdoesn't exist (worked only via asaveDbinternal fallback)c.envexplicitlyfs.ts/fs/getshare branchraw_urlbuilt with unencoded sub-path -? # %in share file names truncated/broke the URLencodeURIComponent(shareId)+encodeDownloadPath(subPath)fs.ts/add_offline_downloadcode: 400with HTTP 200400fs.ts(4 sites)decodeURIComponent(File-Path header)outside try - malformed%sequences threwURIError-> 500decodeUploadHeader()helper returningnull-> HTTP 400 (aligned withraw.ts)fs.tsfetchArchiveBytesdriver.get()whilephysicalwas resolved from the base_path-joined actual pathactual(matchesgetItem//fs/linkconvention)webdav.tsPROPFINDDepthheader read but both branches returned 207 with all children (RFC 4918 requires Depth: 0 to return the collection only)Depth: 0-> self onlyassets.tsresolveCdnBasedb.get("SELECT ...")- the TSgetDb()returns a plain object, so this always threw a swallowedTypeErrorand$versionwas stuck atlatestdb.settingsarrayinternal
internal/stream/stream.tsparseRangeHeaderbytes=-N->start=NaN->fs.createReadStreamthrowsERR_OUT_OF_RANGE-> /d download 500 (players seeking send suffix ranges); multi-range produced truncated slicesnullfor malformed/multi/unsatisfiable ranges, support suffix ranges, clamp endraw.ts(2 sites),proxy_request.tsinternal/archive/zip.tswriter.write()/close()not awaited - inflate errors became unhandled rejections and output could be silently truncatedawaitbothinternal/upload/multipart.tssnapshotreceived.size * chunk_sizeover-counted the smaller last chunk (15MB file / 10MB chunks -> 20MB)edge / root
esa-entry.tsedgeKv.get()on every request, burning 1 of 8 KV subrequests (comment claimed it was cached)esa-entry.tsdeletecatch {}swallowed delete failures, then invalidated caches - callers believed the delete succeeded while the value reappeared on next readput); invalidate only after successmiddleware.js/davand/s3(both mounted insrc/backend/index.ts) - html-accepting GETs were rewritten to the SPA shellloadEnv.js.envvalues kept paired quotes and trailing\r-JWT_SECRET="abc"loaded as an 8-char quoted secretdrivers
webdav/util.tsdisplaynamenot XML-unescaped (a&b.txtlisted literally, breaking rename/move/get); self-detectionendsWith(normTarget)matched everything at root and swallowed same-named subdirsscheme://hostand match exactlys3/util.tsETagnot unescaped ("hex"kept as-is); ListObjectsV1 fallback marker used the virtual path without a trailing slash for folders - duplicate entries / page loopgetKey(path, isFolder)Verification
tsc --noEmit: 0 new errors (2 pre-existing errors indb_cipher.test.tsalso present onmain)test:server: 124/129 pass - identical tomainbaseline (same 5 pre-existing failures in seed/init/CAS areas untouched by this PR)test:store12/12,test:model39/39,test:drivers111/111test:regress: 39/40 - identical tomainbaseline (getStoreConfigErrorexport expectation pre-dates this PR)Deliberately deferred (design-level, needs maintainer input)
getActualPath..traversal vs. userbase_pathcontainment (seed.ts checks it; other fs paths rely on downstreamresolvePathclamping)list()Cloudflare/EdgeOne field compatibility (namevskey,list_complete)id || usernamefallback semanticsstatefor non-OIDC platforms, CSRF middleware mounting, WebDAV Basic-Auth brute-force lockout,keyFormatatomic save, audit-log KV concurrency