Skip to content

Security review fixes (v1.10.2) - #2

Merged
TheJakeBraun merged 12 commits into
masterfrom
security/2026-09-review-fixes
Sep 29, 2026
Merged

TheJakeBraun merged 12 commits into
masterfrom
security/2026-09-review-fixes

Conversation

@TheJakeBraun

Copy link
Copy Markdown

Fixes from a full security review of the fork. The review's headline result: nothing a web visitor sends can reach the node's RPC. Every RPC action is hard-coded and read-only, and the page reads no request input. The fixes below close the remaining gaps. Existing installs keep working unchanged.

# Fix
M1 Docker mounts. The README now mounts ~/nanoNodeMonitor:/opt/nanoNodeMonitor instead of your whole home directory, and the node mounts ~/Nano:/root/Nano. Before, the node's wallet files sat inside the monitor container. entry.sh no longer runs chmod 755 on the mount root; it grants traverse-only access. Older -v ~:/opt setups keep working.
I5 README: node RPC safety. Keep enable_control off, and keep RPC off public interfaces.
M2 Config migration backups. Now written as config.backup-<ts>.php, with the original file's permissions. The old config.php.bak-* files could be downloaded as plain text on manual installs. A failed write now really restores the backup.
L1 Config migration only flattens plain configs. The check is now an allowlist of safe tokens. Before, a config using getenv(), backticks, new, eval or ?: was executed and rewritten with the evaluated values baked in. Such configs are now skipped: left as-is, and still working.
L3 FileCache. It refuses cache directories that are shared, symlinked or owned by another user, and falls back to no caching. The latest-version string is now escaped.
L2 Cache rebuilds. An expired entry is rebuilt once instead of per request (FileCache flock, APCu lock). Before, bursts of visitors multiplied RPC load on a slow node.
L4 Blocked paths. modules/ and scripts/ now return 403. Repo metadata and docs return 404 when the repo is cloned into the webroot.
L6 API errors. Rendered as text, never with innerHTML.
I1 Headers. The page sends X-Content-Type-Options: nosniff and Referrer-Policy. Framing is still allowed so operators can embed the page.
I3 Timeout. The GitHub release lookup uses the 3-second external timeout instead of 30 seconds.
I4 Workflow pinning. Workflow actions are pinned to commit SHAs, and checkout uses persist-credentials: false.

PROJECT_VERSION is now 1.10.2.

Verification

Lint. php -l is clean.

Config migration. --dry-run output is identical before and after for config.sample.php and for typical plain configs.

Local smoke test against a mock node:

  • JSON is valid with no warnings.
  • Repeat requests are served from cache.
  • With the node down, /api.php returns 503.
  • The error box markup is unchanged.

Cache tests:

  • With 6 concurrent requests, the entry is rebuilt once.
  • A world-writable, root-owned or symlinked cache directory disables caching.

Docker image:

  • New and old mount styles both work.
  • The v0 config migrates on start.
  • /modules/config.php, /modules/ and /scripts/ return 403.
  • /README.md, /.git/ and /Dockerfile return 404.
  • The headers are present.
  • A home directory at mode 700 is not made listable.

🤖 Generated with Claude Code

TheJakeBraun and others added 12 commits September 28, 2026 20:49
With the documented `-v ~:/opt` mount, /opt is the operator's home
directory, and entry.sh reset it to 755 on every start. In the Compose
example the node's wallet directory also sat inside that mount.

entry.sh now only adds traverse permission to /opt (so legacy ~:/opt
mounts keep working) and makes the config directory itself readable.
README mounts only ~/nanoNodeMonitor for the monitor and ~/Nano for the
node (same host locations as before) and notes that old setups keep
working but should switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The monitor needs only read-only RPC. Tell operators to keep enable_control off and never expose the RPC port publicly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backup used to be config.php.bak-<ts>, which a web server serves as
plain text when the config lives in the webroot (manual installs), and
it was created with the default umask. Use a .php name next to the
config, copy the original's permission bits, restore the backup when
writing the new config fails, and ignore the backups in git and the
Docker build context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The custom-logic scanner in migrate-config.php was a denylist, so
getenv(), backticks, new, eval, ?:, fn, match and any function call
passed, were evaluated as root at container start and written back as
literals (materializing secrets, or producing a config that fatals).

After the existing denylist, require every token to be on an allowlist
of plain assignments (literals, arrays, true/false/null, defined
constants, non-superglobal variables, = ; , ( ) [ ] . - +) and reject
"(" directly after a value, which would be a call. Anything else is
treated as scripted and left untouched. config.sample.php and plain
configs migrate exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The default file cache lives in the shared system temp dir. A local user
could pre-create it (world-writable or as a symlink) and plant cache
entries, which reached the page unescaped via getVersionInformation(),
or plant symlinks so writes clobber other files.

FileCache now only uses its base directory when it is a real directory
(not a symlink), owned by the effective user and not world-writable, and
never reads or writes a cache file that is a symlink. An unsafe
directory disables the cache (NullCache behaviour, logged), never fatal.
The latest monitor version is escaped with e(); normal version strings
render byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While an entry was expired every concurrent request rebuilt it, so a
burst of api.php requests fired about 11 RPCs each at the node.

FileCache::fetch() now rebuilds under an exclusive flock() on
<cachefile>.lock and re-reads after acquiring it; ApcuCache::fetch()
does the same with apcu_add() as an atomic lock (released on exit too).
Waiting is bounded to 10 s, after which, or when locking is impossible,
both fall back to the previous read-callback-write behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Direct requests to modules/*.php leaked paths through PHP errors, and
installs that git-clone into the webroot served .git/, the docs and the
Docker files.

modules/.htaccess and scripts/.htaccess deny all requests (nothing the
browser loads lives there). The root .htaccess answers 404 for .git/,
.github/, dotfiles, *.md/yml/yaml/sh/ps1 and Dockerfile using only
mod_alias, so it needs no AuthConfig override and cannot 500 the page
on hosts that only allow FileInfo. .dockerignore now keeps the root
.htaccess in the image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The error handler assigned the raw response body to innerHTML, so a proxy/CDN error page or any future error path echoing data would be injected as markup. Extract the text with DOMParser and show it in the same .myError box via textContent (fallback 'API error').

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
X-Frame-Options/frame-ancestors are intentionally not set so operators can keep embedding the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GitHub lookup rendered in the footer used a 30 s timeout, so index.php could block for about 32 s when GitHub is slow. Use EXTERNAL_TIMEOUT like the other external lookups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The publish job holds packages: write, so a moved tag on a third-party action could push a malicious :latest image. Pin every action to its full commit SHA (version in a trailing comment; Dependabot keeps SHA pins updated) and stop persisting the checkout token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TheJakeBraun
TheJakeBraun merged commit b9fbf42 into master Sep 29, 2026
2 checks passed
@TheJakeBraun
TheJakeBraun deleted the security/2026-09-review-fixes branch September 29, 2026 04:04
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