Document the PHP api layer and fix what documenting it exposed
Writing down what each endpoint actually guarantees made the places it didn't obvious — shell arguments reaching a crontab or a bash -c unescaped, master.conf written without tmp+rename, and conf edits that could be saved without ever being parsed.
This commit is contained in:
@@ -1,5 +1,61 @@
|
||||
<?php
|
||||
// Read-only endpoint: return full content of any script in SCRIPTS_DIR.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// PURPOSE
|
||||
// Script and document reader. Returns the full text of one .sh or .md file inside
|
||||
// SCRIPTS_DIR — the source view behind the scheduler page's script viewer and the docs tab.
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
// Two extensions, one endpoint.
|
||||
// Scripts and their READMEs are read the same way because they are read for the same
|
||||
// reason — someone wants to see what a job actually does. Splitting them would mean two
|
||||
// endpoints with identical validation.
|
||||
//
|
||||
// Returns text, never renders it.
|
||||
// Markdown arrives as source. Rendering is the browser's job, and doing it here would
|
||||
// make this endpoint an HTML producer with an HTML producer's escaping problems.
|
||||
//
|
||||
// Relative ids only.
|
||||
// The id is a path relative to SCRIPTS_DIR, so the caller never learns or supplies the
|
||||
// installation root. That is also what makes the same id valid in internal and appdata
|
||||
// storage modes.
|
||||
//
|
||||
// OPERATIONAL SAFEGUARDS
|
||||
// Read-only. There is no write counterpart in this file; edits go through movescript.php
|
||||
// and import_script.php, which have their own validation.
|
||||
//
|
||||
// Traversal is blocked before the path is composed.
|
||||
// str_contains($id, '..') is checked explicitly, and the pattern
|
||||
// ^[A-Za-z0-9_.\-/]+\.(sh|md)$ excludes null bytes, backslashes, spaces, and every
|
||||
// shell metacharacter. The slash has to be permitted because ids are Category/name.sh,
|
||||
// so the '..' check carries the traversal guarantee on its own rather than being
|
||||
// implied by the character class.
|
||||
//
|
||||
// The extension allowlist is the real access boundary.
|
||||
// Only .sh and .md can be named at all, which is what keeps Configurations/*.conf —
|
||||
// the files holding every credential in the system — outside this endpoint's reach. Any
|
||||
// future extension added here has to be checked against that first.
|
||||
//
|
||||
// A missing file is reported, not opened.
|
||||
// file_exists() precedes file_get_contents(), so a bad id returns a named error rather
|
||||
// than a PHP warning leaking the absolute path into the JSON body.
|
||||
//
|
||||
// Known limit: symlinks inside SCRIPTS_DIR are followed.
|
||||
// Containment is enforced on the id, not on the resolved path, so a symlink placed
|
||||
// inside the tree pointing outside it would be read. Not tightened with a realpath
|
||||
// check, because the plugin's own boot model installs SCRIPTS_DIR as a symlink and a
|
||||
// naive containment test would break it. The tree is git-managed; a rogue symlink in it
|
||||
// is a repo compromise, which is a larger problem than this endpoint.
|
||||
//
|
||||
// REQUEST
|
||||
// GET ?id=<Category/name.sh|Category/README-name.md>
|
||||
//
|
||||
// RESPONSE
|
||||
// {"ok":true,"content":"<full file text>"}
|
||||
// {"ok":false,"error":"Invalid id"|"Not found"}
|
||||
//
|
||||
// DEPENDS ON
|
||||
// include/config.php SCRIPTS_DIR
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user