Module: Abqari::Page::Path
- Included in:
- Abqari::Page
- Defined in:
- lib/abqari/page/path.rb
Overview
Output-path resolution. The review (§2 finding C1, §8) flagged
output_path as the security-critical method on Page — a
permalink: frontmatter typo or hostile sync source could otherwise
write outside _site/. Lifting it into its own module makes the
surface easy to audit: every filesystem-bound path computation
for a Page lives here, routed through PathSafe.join_under so
traversal is impossible by construction.
Public surface: output_path, url, layout_name.
Helpers (render_permalink, relative_*, index_page?) are
private — internal to the path computation.
Constant Summary collapse
- PERMALINK_ALLOWED =
Permitted characters in a frontmatter
permalink:value. We allow the usual URL-safe set plus/for nested directories. Anything else (control chars, backslashes,:,?,#, …) is rejected before we touch the filesystem — they're either traversal vectors or just wrong for a URL. %r{\A[A-Za-z0-9_\-./]+\z}.freeze
- LAYOUT_ALLOWED =
Permitted
layout:value. A layout name is resolved to a file path (app/views/layouts/<name>.html.erb) viafind_in_paths, which is thenFile.read+ ERB-eval'd — so a traversing value (../../../../etc/foo) is a file-read + code-execution vector, not just a bad URL. Frontmatterlayout:can arrive from an untrusted platform export (the Jekyll importer preserves it), so allow only flat/nested layout identifiers: alphanumerics,_,-, and/for subdirectories. No leading/, no.(so..can't form), no backslash. %r{\A[A-Za-z0-9][A-Za-z0-9_\-/]*\z}.freeze
Instance Method Summary collapse
- #layout_name ⇒ Object
- #output_path ⇒ Object
-
#url ⇒ Object
Site-relative URL for this page, derived by removing the output root from the absolute output path.
Instance Method Details
#layout_name ⇒ Object
95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/abqari/page/path.rb', line 95 def layout_name if (custom = frontmatter['layout']) name = custom.to_s unless name.match?(LAYOUT_ALLOWED) raise PathSafe::Error, "invalid layout #{custom.inspect} in #{source_path}: " \ 'only A-Z, a-z, 0-9, `_`, `-`, `/` are allowed (no traversal)' end return name end return site.collection_config(collection)['layout'] if collection 'application' end |
#output_path ⇒ Object
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/abqari/page/path.rb', line 57 def output_path # Frontmatter override always wins — useful for /404.html, # /robots.txt-style files, a flat-collection index page, or # any page that needs a non-default URL. # # `permalink:` is user input. Run it through `PathSafe.join_under` # so a malicious or accidental `../../..` segment fails the # build rather than writing outside `output_dir`. if (custom = frontmatter['permalink']) cleaned = custom.to_s.sub(%r{\A/}, '') # `permalink: "/"` is the home page — cleaned becomes empty # after stripping the leading slash. That's a legitimate # value (a site setting an explicit home-page permalink for # documentation purposes); short-circuit to the output dir's # `index.html` before the regex check, which `+` won't # match against the empty string. return PathSafe.join_under(site.output_dir, 'index.html') if cleaned.empty? unless cleaned.match?(PERMALINK_ALLOWED) raise PathSafe::Error, "invalid permalink #{custom.inspect} in #{source_path}: " \ 'only A-Z, a-z, 0-9, `_`, `-`, `.`, `/` are allowed' end if cleaned.end_with?('/') || File.extname(cleaned).empty? PathSafe.join_under(site.output_dir, cleaned, 'index.html') else PathSafe.join_under(site.output_dir, cleaned) end elsif collection rendered = render_permalink(site.collection_config(collection)['permalink']) PathSafe.join_under(site.output_dir, rendered, 'index.html') elsif index_page? PathSafe.join_under(site.output_dir, relative_dir, 'index.html') else PathSafe.join_under(site.output_dir, relative_basename, 'index.html') end end |
#url ⇒ Object
Site-relative URL for this page, derived by removing the output root from the absolute output path.
The strip is ANCHORED. An unanchored sub removes the first
occurrence anywhere in the string, so it silently did nothing
when output_dir wasn't a prefix (a relative
ABQARI_OUTPUT_DIR) and left an absolute filesystem path as the
"URL". Site#output_dir now guarantees an absolute path, so
that specific case can't recur — but anchoring makes the
assumption explicit rather than incidental, and a mismatch fails
loudly instead of shipping local paths to production.
46 47 48 49 50 51 52 53 54 55 |
# File 'lib/abqari/page/path.rb', line 46 def url root = site.output_dir.chomp('/') unless output_path.start_with?("#{root}/") raise "output path #{output_path.inspect} is not under output_dir #{root.inspect}" end output_path .sub(/\A#{Regexp.escape(root)}/, '') .sub(%r{/index\.html\z}, '/') end |