Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ The framework must run on Lucee 5/6/7, Adobe CF 2018/2021/2023/2025, and BoxLang
11. **`local.X = ...` inside `catch` doesn't persist on BoxLang.** Catch body runs under a nested `local` that gets discarded on exit, so `expect(local.X)` after the catch reads the un-touched outer value. Use a struct field: `var state = {flag = false}; ... state.flag = true;`. Bare `var bareName` + unscoped `bareName = true` also works but the struct form mirrors `TenantResolverSpec` and is the prior-art pattern.
12. **`for (local.i = ...)` inside `finally` miscompiles on Lucee 7.** Lucee 7.0.1+100 throws `variable [local] doesn't exist` at runtime when a `for` loop declares or iterates `local`-/`var`-scoped variables inside a `finally` block (one probe shape even produced a JVM `Expecting a stackmap frame` verifier error). Bare assignments and function calls in `finally` are fine; loops are not. Hoist the loop into a `public` `$`-prefixed helper and call it from `finally` — reference: `$restoreEmailViewVariables()` in `vendor/wheels/controller/miscellaneous.cfc` ([#2922](https://github.com/wheels-dev/wheels/pull/2922)).
13. **Bare tag-in-script statements without parentheses (e.g. `cfabort;`) are Lucee-only.** Adobe CF compiles the bare token as a reference to an undefined VARIABLE and throws `Variable CFABORT is undefined` at runtime (every Adobe engine, not just one release). Use the script keyword (`abort;`) or the parenthesized call form (`cfheader(...)`-style) instead. The `enablePublicComponent=false` 404 branch in `vendor/wheels/Dispatch.cfc` shipped a bare `cfabort;`, which turned `GET /` on every stock Adobe install in `testing`/`production` into an HTTP 500 ([#3029](https://github.com/wheels-dev/wheels/issues/3029)). Structural guard: `vendor/wheels/tests/specs/security/BareCfabortGuardSpec.cfc` fails the suite if any bare script-context `cfabort` statement reappears under `vendor/wheels/**/*.cfc` (tag-context `<cfabort>` in `.cfm`/tag-based CFCs stays legal).
14. **Adobe 2025's JVM rejects member calls on JDK-internal classes (JPMS).** Calling any member on an object whose runtime class lives in an unexported package (`com.sun.*`, `jdk.internal.*`) — e.g. the `com.sun.crypto.provider.PBKDF2KeyImpl` returned by `SecretKeyFactory.generateSecret()` — throws `java.lang.reflect.InaccessibleObjectException` on Adobe 2025 (its reflection layer bulk-`setAccessible`s the concrete class's methods; Lucee, BoxLang, and Adobe ≤2023 tolerate the same call, so **local Adobe 2023 green does NOT cover this**). Route the call through the exported interface's `Method` object instead: `CreateObject("java","java.lang.Class").forName("javax.crypto.SecretKey").getMethod("getEncoded", JavaCast("null","")).invoke(keyObj, JavaCast("null",""))` — `getMethod`/`invoke` treat the null varargs as empty. Hit by `PasswordHasher.$deriveKey()` ([#3300](https://github.com/wheels-dev/wheels/issues/3300)); watch for it with any Java factory API that returns internal implementation types.

Verify Adobe CF fixes locally before pushing — don't iterate via CI:
```bash
Expand Down
14 changes: 13 additions & 1 deletion vendor/wheels/auth/PasswordHasher.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,19 @@ component output="false" {

try {
local.factory = CreateObject("java", "javax.crypto.SecretKeyFactory").getInstance("PBKDF2WithHmacSHA256");
local.derivedKey = local.factory.generateSecret(local.keySpec).getEncoded();
local.secretKey = local.factory.generateSecret(local.keySpec);

// Do NOT call members on the returned key directly: it is a
// com.sun.crypto.provider.PBKDF2KeyImpl, a JDK-internal class that
// java.base does not open. Adobe 2025's JVM rejects the reflective
// member access with InaccessibleObjectException (its reflection
// layer makes the concrete class's methods accessible en masse).
// Invoke getEncoded() through the exported javax.crypto.SecretKey
// interface instead — public interface methods need no opens.
local.getEncoded = CreateObject("java", "java.lang.Class")
.forName("javax.crypto.SecretKey")
.getMethod("getEncoded", JavaCast("null", ""));
local.derivedKey = local.getEncoded.invoke(local.secretKey, JavaCast("null", ""));
} finally {
// Zero the internal password copy held by the spec.
local.keySpec.clearPassword();
Expand Down
Loading