Skip to content

LeafSources, LeafSource and File Sandboxing/Limiting in NIOLeafFiles

Pre-release
Pre-release

Choose a tag to compare

@tanner0101 tanner0101 released this 19 Jun 22:22
2c67111
This patch was authored and released by @tdotclare.

LeafSources, LeafSource and File Sandboxing/Limiting in NIOLeafFiles

This update provides several notable changes to internal behaviors to how raw source templates for Leaf are located and read prior to parsing:

  • LeafSources stores multiple LeafSource*-adhering objects by name and maintains a default search order of which objects to attempt to read from
  • LeafSource (previously LeafFiles) represents any object with a directed behavior for interpreting a template name into its own reading space (eg, a filesystem or database)
  • NIOLeafFiles gains initialization configuration for sandboxing and reading-limit behavior

LeafSources Usage Pattern

LeafSources publishes three public attributes:

  • .all: Set<String> read-only set of all registered LeafSource objects
  • .searchOrder: [String] read-only array of LeafSources which will be checked if no specific source is specified
  • register(source key: String = "default", using source: LeafSource, searchable: Bool = true) will add the source by key name, and if searchable is set, will include it in the search order.

Keys must be unique, and search order cannot be changed after a source is added to prevent collisions or changes in behavior by modifying sources once LeafRenderer has been used to read any template, so register must be called in the order objects should be used.


NOTE

Hooks are not yet available for specifying a single source to use, but are intended to be added. render by default will progressively search the sources according to searchOrder, but a new method for directively using only a single named source (EG: render(template, from: source)) would force Leaf to only attempt to use that named source (failing if source can't provide it).

This will allow applications to directively secure templates so that, EG, internally called templates can be named as being in a specific source, while potentially publically written templates can be stored in another source (and prevented from accessing the secured templates by not including that source in the default search order.)


A default definition of LeafSources.singleSource(LeafSource) provides a convenience for creating a complete LeafSources object that mimics the previous behavior of LeafRenderer.files

Example usage:

// Will access templates exactly as before
let nioLeaf = NIOLeafFiles(fileio: app.fileio,
                           limits: [.requireExtensions],
                           sandboxDirectory: "/",
                           viewDirectory: app.directory.viewsDirectory)
let singleSource = LeafSources.singleSource(nioLeaf)

// Will search sourceOne, then SourceTwo if sourceOne can't provide requested template
// Will never search hiddenSource unless it is specifically requested via explicit request
let multipleSources = LeafSources()
try! multipleSources.register(using: sourceOne)
try! multipleSources.register(source: "sourceTwo", using: sourceTwo)
try! multipleSources.register(source: "hiddenSource", using: hiddenSource, searchable: false)

This results in a LeafSource with three specific sources of templates; a request for a template will first check sourceOne, then sourceTwo. hiddenSource will not be checked unless specifically named when attempting to render a template.

LeafSource Usage Pattern

LeafSource is a renaming of the LeafFiles protocol and updates the single signature call required to reflect generic concepts of a "template name" versus a filesystem path. escape may be meaningless for a particular source object, and can be safely ignored if so.

// Required signature format
func file(template: String, escape: Bool, on eventLoop: EventLoop) throws -> EventLoopFuture<ByteBuffer>
// Deprecated format
func file(path: String, on eventLoop: EventLoop) throws -> EventLoopFuture<ByteBuffer>

For objects with a concept of "escaping", an attempt to escape should throw LeafError(.illegalAccess()) so that LeafSources can fail a search entirely. EG; if the first configured source cannot provide the requested template, LeafSources will continue to the next source, but if the first throws illegal access, the entire render attempt will fail rather than going on to a potentially less-restricted source.


NOTE

template should be interpreted in a meaningful way by an adherent to LeafSource, so calls to render should generally refer to the template name as generically as possible and allow the specific LeafSource to expand or interpret it as appropriate for its internal store map; LeafRenderer no longer makes any attempt to expand or modify the template name prior to requesting it from the LeafSource:

// Previous: LeafFiles would get a request for "/#viewDirectory#/path/to/template.leaf"
render("path/to/template")
// Now: LeafSource gets a request for exactly "path/to/template"
render("path/to/template")

However, default implementations bridging the older call signature for LeafFiles will mimic the old LeafRenderer behavior and expand the template path using the old behavior (this does NOT protect against escaping or guarantee non-relative path requests)


NIOLeafFiles Usage Pattern

NIOLeafFiles gains the following initialization configuration for sandboxing and reading-limit behaviors:

  • viewDirectory: the default directory templates will be interpreted as relative to
  • sandboxDirectory: the highest level directory the object may read from
  • limits: (NIOLeafFiles.Limit): an OptionSet of behaviors:
    • .toSandbox: if set, files outside sandboxDirectory can't be read
    • .toVisibleFiles: if set, prevents reading any file/directory starting with . (or files inside such directories)
    • .requireExtensions: only files with an extension can be read
    • .onlyLeafExtensions: only files with .leaf extensions can be read
  • Default configuration of limits is [.toSandbox, .toVisibleFiles, .requireExtensions]
  • viewDirectory must be contained inside sandboxDirectory(or coincident to limit entirely to viewDirectory)

Example:

let templateFolder = "/web/app/Resources/"
NIOLeafFiles(fileio: fileio,
             limits: .default, // .toSandbox, .toVisibleFiles, .requireExtensions
             sandboxDirectory: templateFolder,
             viewDirectory: templateFolder + "Views/")
... // setup happens for LeafRenderer

renderer.render("a")       // Tries to render "/web/app/Resources/Views/a.leaf"
renderer.render("a.leaf")  // Tries to render "/web/app/Resources/Views/a.leaf"
renderer.render("../a")    // Tries to render "/web/app/Resources/a.leaf"
renderer.render("../../a") // Throws .illegalAccess for "/web/app/a.leaf"
renderer.render(".ssh")    // Throws .illegalAccess for ""/web/app/Resources/Views/.ssh"