Skip to content

fix: repair failing test baseline (path validation, 404 vs 403, test isolation) - #82

Open
greirson wants to merge 1 commit into
mainfrom
fix/test-baseline
Open

fix: repair failing test baseline (path validation, 404 vs 403, test isolation)#82
greirson wants to merge 1 commit into
mainfrom
fix/test-baseline

Conversation

@greirson

@greirson greirson commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

On main, npm test was not green: 65 tests, 47 pass, 18 fail, plus latent
parallel-run flakiness. Two of the failures turned out to be real production
bugs
, not test noise. This PR fixes the bugs, corrects the outdated test
expectations, and makes the suite deterministic.

Result: npm test is 67/67 green and deterministic (verified across repeated
parallel runs, with and without a local .env); npm run lint is clean.

Production bugs fixed

  1. Symlinked upload directory rejected valid uploads (src/utils/fileUtils.js)
    isPathWithinUploadDir resolved the upload dir with fs.realpathSync but
    resolved a non-existent candidate path with only path.resolve. When the
    upload dir is reached through a symlink (macOS /var -> /private/var, and
    notably Docker bind mounts), the two sides disagreed and path.relative
    produced a .. prefix, so valid in-bounds paths were wrongly rejected. Added
    realpathAllowingMissing, which resolves symlinks in the deepest existing
    ancestor and re-appends the missing remainder, falling back gracefully on
    ENOTDIR/EACCES/ELOOP instead of throwing.

  2. Missing in-bounds files returned 403 instead of 404 (src/routes/files.js)
    The info/download/delete/rename routes used the path check as an existence
    gate (requireExists=true) and mapped its false to 403 "Access denied"
    plus a misleading "path traversal attack" log. A simply-missing file therefore
    returned 403 (and a false security alert) instead of 404. Switched these to
    requireExists=false so missing in-bounds paths fall through to the fs call
    and return 404. Path traversal is still rejected with 403 (verified).

Test corrections

  • auth.test.js configured the PIN via process.env.PIN, but the app reads
    DUMBDROP_PIN, and it was set inside before() after config is loaded and
    frozen, so the PIN was never actually configured. Fixed, and corrected the
    empty-PIN expectation to 401 (the route's actual response for an invalid PIN).
  • Corrected outdated expectations for hyphen-to-underscore filename sanitization
    and the batch-ID format contract.

Flakiness fix

All suites shared the real ./local_uploads directory and ran in parallel, with
some suites deleting the whole directory in teardown. Each suite now isolates its
UPLOAD_DIR to a unique per-process temp dir (parallel-safe), which also stops
tests from polluting the dev upload directory. A pre-clean in before() guards
against pid reuse.

Verification

  • npm test: 67/67 green, deterministic across parallel runs (with and without .env).
  • npm run lint: clean (also fixed 2 pre-existing no-unused-vars errors in the touched file).
  • Confirmed in a running browser/app: upload/list/download/delete work; missing
    in-bounds files now return 404; path traversal still returns 403.

Not in scope (noted for follow-up)

  • The download route performs fs.access/createReadStream on the unresolved
    path (a pre-existing TOCTOU window, defense-in-depth only); left for a separate
    hardening pass.

High-level PR Summary

This PR fixes two production bugs and makes the test suite reliable and deterministic. The first bug caused symlinked upload directories (common in Docker bind mounts and macOS /var paths) to incorrectly reject valid uploads due to inconsistent symlink resolution. The second bug caused missing but in-bounds files to return 403 "Access denied" instead of the correct 404 status. Additionally, the test suite had incorrect expectations (PIN configuration, filename sanitization, batch ID format) and suffered from parallel-run flakiness due to shared upload directories. All test suites now use isolated per-process temp directories, and the suite is now 67/67 green and deterministic.

⏱️ Estimated Review Time: 30-90 minutes

💡 Review Order Suggestion
Order File Path
1 test/auth.test.js
2 test/files.test.js
3 test/security.test.js
4 test/upload.test.js
5 src/utils/fileUtils.js
6 src/routes/files.js
7 test/path-validation.test.js

Need help? Join our Discord

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of missing files in upload directory—requests for non-existent files now return appropriate "not found" responses instead of access errors.
    • Enhanced symlink resolution for better compatibility across different systems.
    • Filenames with hyphens are now automatically converted to underscores for consistency.

…tion)

The committed suite had 18 deterministic failures plus latent flakiness on main.
Root causes and fixes:

- isPathWithinUploadDir rejected valid in-bounds paths when the upload dir is
  reached through a symlink (macOS /var, Docker bind mounts): the non-existent
  branch used path.resolve while the upload dir was realpath'd. Added
  realpathAllowingMissing to resolve symlinks in the deepest existing ancestor,
  falling back gracefully on ENOTDIR/EACCES/ELOOP instead of throwing.
- files.js info/download/delete/rename returned 403 "path traversal" for missing
  but in-bounds paths. Switched the bounds check to requireExists=false so they
  return 404; traversal attempts are still rejected with 403.
- auth.test.js set the PIN via the wrong env var (PIN, not DUMBDROP_PIN) and too
  late (after config froze); fixed, and corrected the empty-PIN expectation to
  401 (the route's actual response).
- Corrected outdated test expectations: hyphen-to-underscore sanitization and
  the batch-ID format contract.
- Isolated each suite's UPLOAD_DIR to a unique per-process temp dir, removing
  cross-suite races on ./local_uploads (parallel-safe) and stopping tests from
  polluting the dev upload directory; pre-clean in before() guards pid reuse.
- Fixed 2 pre-existing no-unused-vars lint errors in the touched file.

Result: npm test 67/67 green, deterministic across parallel runs with and
without a local .env; npm run lint clean. Route behavior (404 vs 403, traversal
still 403) and the upload flow verified in a browser.
@control-dw

control-dw Bot commented Jun 13, 2026

Copy link
Copy Markdown

The preview deployment for DumbWareio/DumbDrop-main is ready. 🟢

Open Preview | Open Build Logs | Open Application Logs

Last updated at: 2026-06-13 03:56:21 CET

@control-dw

control-dw Bot commented Jun 13, 2026

Copy link
Copy Markdown

The preview deployment for DumbWareio/DumbDrop-demo is ready. 🟢

Open Preview | Open Build Logs | Open Application Logs

Last updated at: 2026-06-13 03:56:22 CET

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR refactors path traversal defense by introducing symlink-aware resolution for non-existent targets, updating the traversal guard to validate only path inclusion (not existence), and switching four route endpoints to this "guard-only" mode. Test suites are refactored to use isolated temp directories for parallel safety with updated assertions.

Changes

Path Traversal and Route Behavior Refactoring

Layer / File(s) Summary
Symlink-aware path resolution for missing targets
src/utils/fileUtils.js
New realpathAllowingMissing(targetPath) helper resolves the deepest existing ancestor via fs.realpathSync and re-appends unresolved path segments, enabling consistent symlink interpretation when the full path does not exist (e.g., macOS /var/private/var, Docker bind-mount symlinks).
Updated isPathWithinUploadDir for traversal-only mode
src/utils/fileUtils.js
isPathWithinUploadDir now uses realpathAllowingMissing when requireExists=false, replacing lexical path.resolve normalization to ensure symlink-consistent comparison with the realpath'd upload directory.
Four endpoints switch to traversal-only guard mode
src/routes/files.js
GET /info/*, GET /download/*, DELETE /*, and PUT /rename/* now call isPathWithinUploadDir(..., false), making the traversal check guard-only; missing-file handling defers to downstream fs.stat/fs.access calls instead of the traversal guard.
Test suite infrastructure: isolated temp directories
test/auth.test.js, test/files.test.js, test/security.test.js, test/upload.test.js
All five test suites set process.env.UPLOAD_DIR to unique pid-scoped temp directories before app import for parallel safety; before hooks delete stale contents, after hooks perform single recursive cleanup instead of manual file iteration.
Test assertion updates for new behavior
test/auth.test.js, test/files.test.js, test/path-validation.test.js, test/security.test.js, test/upload.test.js
Auth empty-PIN expects HTTP 401; files rename test verifies filename sanitization and asserts missing in-bounds source returns 404 not 403; path-validation adds ENOTDIR edge case coverage; security/upload tests correct filenames and batch-ID format to match sanitizer behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • DumbWareio/DumbDrop#73: Both PRs modify isPathWithinUploadDir(filePath, uploadDir, requireExists) semantics for non-existent targets and update route endpoint validation/tests accordingly.
  • DumbWareio/DumbDrop#70: Both PRs adjust path-traversal protection in src/routes/files.js via isPathWithinUploadDir behavior changes.

Poem

🛡️ Symlinks danced, paths once missed,
Now realpathAllowingMissing turns mist to grist.
Four guards stand watch, then step aside—
Let filesystems finish what traversal denied.
Tests run safe in temp-dirs clean,
The safest dance that ever was seen! 🎭

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically summarizes the main fixes: path validation bugs, 404 vs 403 corrections, and test isolation improvements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/test-baseline

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed due to a network error.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/routes/files.js (1)

86-103: ⚡ Quick win

Acknowledged TOCTOU gap — reminder for follow-up hardening.

Look, I know you said this is "out of scope," but let me spell it out so it doesn't get lost in the shuffle: the traversal check resolves symlinks via realpathAllowingMissing, but then fs.access and createReadStream operate on the original filePath. A race condition exists where a symlink could be created between validation and file access, pointing outside the upload directory.

You've already noted this in the PR objectives for a separate hardening pass. Just making sure nobody forgets. The fix would be to use the resolved path for all subsequent filesystem operations, not just the validation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/files.js` around lines 86 - 103, The traversal check uses a
resolved path helper but subsequent filesystem ops still use the original
filePath, leaving a TOCTOU via symlink changes; fix by obtaining the resolved
path (via realpathAllowingMissing or the same resolution used inside
isPathWithinUploadDir) and then use that resolved path for fs.access and for
require('fs').createReadStream (and any other fs operations) instead of the
original filePath so the validation and access operate on the same canonical
target.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/routes/files.js`:
- Around line 86-103: The traversal check uses a resolved path helper but
subsequent filesystem ops still use the original filePath, leaving a TOCTOU via
symlink changes; fix by obtaining the resolved path (via realpathAllowingMissing
or the same resolution used inside isPathWithinUploadDir) and then use that
resolved path for fs.access and for require('fs').createReadStream (and any
other fs operations) instead of the original filePath so the validation and
access operate on the same canonical target.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 888f7833-6fbd-438c-a6a7-08fabc112617

📥 Commits

Reviewing files that changed from the base of the PR and between ff8f813 and 569aa70.

📒 Files selected for processing (7)
  • src/routes/files.js
  • src/utils/fileUtils.js
  • test/auth.test.js
  • test/files.test.js
  • test/path-validation.test.js
  • test/security.test.js
  • test/upload.test.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant