Skip to content

Fix arbitrary code execution and predictable session tokens in /updateTheme - #1425

Open
herdiyana256 wants to merge 4 commits into
cdapio:developfrom
herdiyana256:fix-updatetheme-arbitrary-require-rce
Open

Fix arbitrary code execution and predictable session tokens in /updateTheme#1425
herdiyana256 wants to merge 4 commits into
cdapio:developfrom
herdiyana256:fix-updatetheme-arbitrary-require-rce

Conversation

@herdiyana256

@herdiyana256 herdiyana256 commented Jul 25, 2026

Copy link
Copy Markdown

Two issues in server/token.mjs and server/uiThemeWrapper.js combine into an exploitable chain through the POST /updateTheme route in server/express.js.

getSecretFromCDAPConfig() in server/token.mjs fell back to path.resolve(__dirname, 'config', 'development', 'session.secret.key') -- a file path string, not that file's contents -- used directly as key material whenever cdapConfig['session.secret.key'] isn't configured. That value is fully predictable by anyone who knows the install directory, so a session token can be forged with no access to any real secret. The fallback also doesn't match any real file on disk (the actual file is session_secret.key, with an underscore), which points to this being a missing fs.readFileSync() call rather than intentional behavior.

extractUITheme() in server/uiThemeWrapper.js loaded the theme file with non_webpack_require, which executes .js files as Node modules. POST /updateTheme passes req.body.uiThemePath straight into extractUITheme() with no validation beyond the (forgeable) session-token check above. Together: forge a token, then point uiThemePath at any .js file the server can read, and its code runs with the server process's privileges.

token.mjs now throws instead of falling back to a predictable value when session.secret.key isn't configured, so token generation/validation fails closed. uiThemeWrapper.js reads and JSON.parse()s theme files instead of require()-ing them, since they're data, not code. express.js additionally resolves uiThemePath in /updateTheme to a filename inside config/themes/ only, matching the route's own doc comment describing it as a way to switch between the shipped theme files for testing.

Added a regression test for each half: token_test.mjs confirms a missing secret makes generateToken throw and validateToken reject everything, including a token forged with the old fallback logic; the new uiThemeWrapper.test.js confirms a plain JSON theme file still loads and a .js file passed as the theme path is no longer executed.

yarn install didn't complete in the environment I used for this (large, older dependency tree), so I couldn't run uiThemeWrapper.test.js through the project's actual Jest config. Verified its logic instead by extracting extractUITheme's unmodified post-fix code into a standalone harness (only __dirname/log4js/lodash shimmed for environment reasons) and running it directly under Node -- confirmed the JSON case still loads and the .js case no longer executes. Happy to re-verify against the real Jest run if that's easier for a reviewer with the deps already installed.

…eTheme

Two issues combine into an exploitable chain:

1. server/token.mjs: getSecretFromCDAPConfig() fell back to
   path.resolve(__dirname, 'config', 'development', 'session.secret.key'),
   a file path string, used directly as key material, whenever
   cdapConfig['session.secret.key'] wasn't set. That value is fully
   predictable by anyone who knows the install directory, letting them
   forge valid session tokens with no access to any real secret. Now
   throws instead of falling back, so token generation/validation fails
   closed when the secret isn't configured.

2. server/uiThemeWrapper.js: extractUITheme() loaded theme files with
   __non_webpack_require__, which executes .js files as Node modules.
   Combined with the POST /updateTheme route in server/express.js, which
   passes req.body.uiThemePath straight into extractUITheme() with no
   validation beyond the (forgeable) session-token check, this let a
   request execute arbitrary code on the server. Theme files are JSON
   data, not code, so extractUITheme now reads and JSON.parses them
   instead of require()'ing them.

Also restricted /updateTheme's uiThemePath to a filename inside
config/themes/ (discarding any directory component), matching the
route's own stated purpose of switching between the shipped theme
files for testing.

Added a regression test for each: token_test.mjs confirms a missing
secret now causes generateToken to throw and validateToken to reject
everything (including a token forged with the old fallback), and
uiThemeWrapper.test.js confirms a .js file passed as the theme path is
no longer executed while a plain JSON theme file still loads.

Full yarn install could not complete in this environment (large legacy
dependency tree), so uiThemeWrapper.test.js could not be run through
the project's actual Jest config here; its logic was verified by
extracting extractUITheme's unmodified post-fix code into a standalone
harness with only __dirname/log4js/lodash shimmed for environment
reasons, run directly under Node.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces critical security fixes to prevent directory traversal and arbitrary code execution. It restricts theme file resolution to a specific directory, replaces unsafe dynamic requires with JSON parsing, and fails closed when the session secret key is unconfigured. The review feedback correctly identifies that the directory traversal defense is incomplete, as inputs like '..' or '.' can bypass path.basename, and a non-string input could cause an unhandled TypeError.

Comment thread server/express.js Outdated
path.basename returns '..' or '.' unchanged for those exact inputs
(it strips directory components, it doesn't resolve '..' segments),
so a request with uiThemePath: ".." resolved one level above
config/themes/ instead of being contained to it. A non-string
uiThemePath (e.g. an array) also threw a TypeError out of
path.basename before reaching the try/catch.

Both from review feedback on PR cdapio#1425.
Comment thread server/token.mjs
Comment thread server/express.js
Comment thread server/uiThemeWrapper.js Outdated
…eTheme in prod

- token.mjs: when NODE_ENV=development and session.secret.key is not
  configured, fall back to reading the actual file contents from
  config/development/session_secret.key via fs.readFileSync() instead
  of throwing. All other environments (including production) still fail
  closed. Fixes the original bug where a file *path string* was used as
  key material.

- express.js: /updateTheme now returns 401 when NODE_ENV=production,
  making the endpoint completely unavailable in production as intended
  by the route's own doc comment.

- uiThemeWrapper.js: trim the RCE-detail comment now that the endpoint
  is disabled in production and the attack surface no longer exists.
@herdiyana256

Copy link
Copy Markdown
Author

All three points addressed in f77678d, threads resolved.

Leftover from an earlier approach to resolving __dirname; never
referenced, and .eslintrc.json's no-unused-vars rule (severity: error)
would fail on it.
@herdiyana256

Copy link
Copy Markdown
Author

@GnsP All threads addressed, and I also removed an unused fileURLToPath import in server/token.mjs that snuck into f77678d (would've failed no-unused-vars). Ready for re-review whenever you have a moment.

@GnsP GnsP added the build triggers github action label Aug 5, 2026
@GnsP

GnsP commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Hi @herdiyana256,

Thank you so much for discovering this vulnerability and taking the time to contribute a solid fix! We deeply appreciate the effort you put into helping keep this project secure.

Also, thank you for your quick turnaround on the changes we discussed during the review. The code looks good to me, and I've just approved the PR so we can get this merged.

We would be absolutely thrilled to see more contributions from you in the future! Whether it's more security improvements, general bug fixes, or new features, your work is very welcome here.

Thanks again!

Best

@herdiyana256

Copy link
Copy Markdown
Author

Thanks again @GnsP, really appreciate the kind words and the quick reviews throughout.

Since this is approved with all checks green and no conflicts with develop, is there anything left on my end before it can land, or is it just waiting on a committer to merge? Happy to rebase or adjust anything if that helps.

Would love to get this one in, and I've got #1426 and #1427 up as well whenever you have bandwidth for those.

@herdiyana256

herdiyana256 commented Aug 18, 2026

Copy link
Copy Markdown
Author

Hi @GnsP gentle bump on this one.

It's been approved for two weeks now with all checks green and no conflicts with develop. Is there a particular committer I should ping to get it merged, or is there a release window it's waiting on?

Same question applies to #1426 and #1427 whenever you have bandwidth. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build triggers github action

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants