Support relative markdown link navigation #4
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/relative-link-navigation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Clicking a relative
.mdlink in rendered output now navigates to that file through mdview instead of 404ing. Enables doc trees with a README TOC linking intodocs/.Changes
base_dirfromMDVIEW_FILEparent directory -- all relative paths resolve from thereGET /*.mdthat renders any markdown file under the base dir.mdhrefs to mdview routes (external URLs untouched)base_diror get 403Test Plan
[link](docs/foo.md)-- click navigates to rendered foo.md../../../etc/passwdreturns 403https://links are not rewrittenReview Checklist
:rootRelated Notes
Closes #3
PR #4 Review
DOMAIN REVIEW
Stack: Ruby on Rails 8, Redcarpet markdown rendering, CSS. Applying Ruby/Rails domain checks.
Path Traversal Guard (app/controllers/markdown_controller.rb:24-27)
The guard uses the standard
File.expand_path+start_with?pattern:This has a prefix collision vulnerability. If
base_dirresolves to/home/user/docs, then a crafted path resolving to/home/user/docs-evil/secret.mdpasses thestart_with?check. The fix is to compare againstbase_dir + "/"(or usePathname#ascend). In practice, sincebase_diris derived fromFile.dirname(ENV.fetch("MDVIEW_FILE"))and this is a local CLI tool, the exploit requires a sibling directory with a matching prefix -- low probability but the fix is trivial and the pattern should be correct on principle.ENV.fetch crash path (markdown_controller.rb:14)
base_dirusesENV.fetch("MDVIEW_FILE")which raisesKeyErrorif the env var is unset. The original code usedENV["MDVIEW_FILE"]with a nil check and a graceful 404. The new code will crash with an unhandled 500. This is a regression in error handling for the startup case.File.join no-op (markdown_controller.rb:18)
File.join(params[:path])with a single string argument just returns that string unchanged. Not a bug, but misleading -- it reads as if it is joining multiple path components. Either useparams[:path]directly or add a comment explaining intent.html_safe on file content (markdown_controller.rb:6)
@html = rewrite_md_links(html, @relative_dir).html_safemarks rendered markdown as safe HTML. Since this is a local CLI tool rendering local files you control, the XSS risk is self-inflicted only. Noted but not blocking.Link rewriting regex (markdown_controller.rb:41)
The regex
(<a\s[^>]*href=")([^"]*\.md)(")will miss:docs/foo.md#sectiondocs/foo.md?v=1For a personal tool this is acceptable, but the anchor case (
foo.md#heading) is a realistic markdown pattern that will silently fail to rewrite.BLOCKERS
1. Path traversal prefix collision (Security)
start_with?(base_dir)without a trailing separator allows escape to sibling directories with matching prefixes. Fix:@file_path.start_with?(base_dir + "/") || @file_path == base_dir. This is the one security-relevant finding. While exploitability is low in a local CLI tool, the guard is the single security mechanism and should be correct.NITS
ENV.fetch regression: Consider rescuing
KeyErrorinbase_diror falling back to a graceful error message. The original nil-guard behavior was better UX for misconfiguration.File.join(params[:path]): Replace with
params[:path]directly, or usePathnamefor clarity.Regex anchor support: Extend the href regex to capture optional
#fragmentportions:([^"]*\.md)(#[^"]*)?(")so thatdocs/setup.md#installlinks get rewritten correctly.View logic:
@relative_dir == "." ? @dirname : @relative_dirin the ERB template -- this conditional display logic could move to a helper method for clarity.No automated tests: The test plan is manual-only. For a small CLI tool this is not blocking, but the path traversal guard specifically is a good candidate for a controller test (one happy path, one
../../../etc/passwdcase, one prefix collision case).SOP COMPLIANCE
feat/relative-link-navigation, should be3-relative-link-navigationper{issue-number}-{kebab-case-purpose}conventionCloses #3references the issue but no plan slugPROCESS OBSERVATIONS
VERDICT: NOT APPROVED
One blocker: the path traversal guard has a prefix collision vulnerability that should be fixed before merge. The fix is a one-line change (
base_dir + "/"). Once addressed, this is ready to merge.PR #4 Re-Review
Re-review after fixes addressing the three findings from the initial review.
FIX VERIFICATION
1. Path traversal guard (was BLOCKER) -- FIXED
The guard now reads:
The trailing
"/"onbase_direliminates the prefix-confusion bypass (e.g.,/home/user/docs-evil/no longer passes a check against/home/user/docs). The|| @file_path == entrycorrectly allows the root entry file itself, which is safe becauseentryis validated for existence immediately above. This is a correct and complete fix.2. ENV.fetch crash regression -- FIXED
base_dirnow usesENV["MDVIEW_FILE"].to_s(safe on nil), andresolve_filedoes a graceful nil/existence check before proceeding:No crash path remains. Correct fix.
3. Regex missed .md#anchor links -- FIXED
Regex updated to
(?:#[^"]*)?capturing optional fragment. The split/rejoin logic correctly preserves anchors during link rewriting. Verified correct.DOMAIN REVIEW (Ruby on Rails)
Path security: The
File.expand_path+start_with?("#{base_dir}/")pattern is the standard Rails approach to path traversal prevention. The guard runs inbefore_action, so no route bypasses it. Good.Controller structure: Clean extraction of
resolve_fileas abefore_action,render_markdownandrewrite_md_linksas private helpers. Single responsibility per method. Thebefore_actionreturning early on error (viarender+return) correctly halts the action chain.html_safeusage:@html = rewrite_md_links(html, @relative_dir).html_safe-- this is the one area worth noting. The HTML comes from Redcarpet rendering user-supplied markdown, then regex post-processing. Redcarpet does not sanitize by default. For a local single-user doc viewer tool, this is acceptable. If mdview ever serves untrusted markdown, a sanitization layer (e.g.,Sanitizegem orRails::HTML5::SafeListSanitizer) should be added beforehtml_safe. Not a blocker for this use case.Route constraint:
get "/*path", to: "markdown#show", constraints: { path: /.*\.md/ }-- correctly limits the catch-all to.mdpaths only. Other asset routes and the root route are unaffected.CSS: Uses CSS variables from
:rootexclusively (--color-muted,--color-link,--spacing-xs). No hardcoded hex values. Follows gruvbox token conventions from prior PR #2.BLOCKERS
None. All three previous blockers are resolved.
NITS
base_dirwhenMDVIEW_FILEis nil:base_dircallsFile.dirname(ENV["MDVIEW_FILE"].to_s)which yieldsFile.dirname("")=".". This is harmless becauseresolve_filereturns early beforebase_diris ever used in a path check whenentryis nil. But a comment documenting this would make the safety reasoning explicit for future readers.Route format constraint: The catch-all
/*pathwithconstraints: { path: /.*\.md/ }will also match paths likefoo.markdownif someone renames, but will not matchfoo.MD(case-sensitive). For a personal doc viewer this is fine, but worth knowing.Test coverage: The PR body has a manual test plan (path traversal returns 403, external links untouched, etc.) but no automated tests. For a personal tool this is not a blocker, but if the project grows, a request spec for the 403 path-traversal case would be high-value for regression prevention.
SOP COMPLIANCE
feat/relative-link-navigationnot3-relative-link-navigation. Minor deviation from{issue-number}-{kebab-case-purpose}convention.Closes #3PROCESS OBSERVATIONS
The review-fix-re-review loop worked cleanly here. All three findings from the initial review were addressed correctly in one pass. The path traversal fix is the most important -- the trailing-slash pattern is a well-known best practice and was applied correctly. Change failure risk is low; this is additive functionality with a proper guard rail.
VERDICT: APPROVED