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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo

### Added

- Document CORS allow-list defaults drift when migrating from 3.x `set(accessControlAllow*)` global settings to `wheels.middleware.Cors`; add header comparison table, explicit-constructor-args fix, and common-issues entry to the 3.x→4.x upgrade guide and a migration callout to the CORS reference page (#2708)
- `wheels deploy init` now scaffolds a starter `Dockerfile` (Lucee 7 + Java 21 multi-stage, `/up` HEALTHCHECK aligned with the generated `kamal-proxy` healthcheck) and a `.dockerignore` alongside `config/deploy.yml` and `.kamal/secrets`. `--force` also gates the `Dockerfile` — an existing user-authored Dockerfile aborts the init without `--force`, while an existing `.dockerignore` is silently preserved (since it's commonly user-curated even before adopting `wheels deploy`). The npm builder stage works for any Wheels app — projects without a JS pipeline pass through unchanged; projects with a `package.json` install + build automatically. Secrets (reload password, DB password, registry password) are injected at deploy time via `.kamal/secrets`, never baked into the image (#2673)

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ These are the constructor arguments on `wheels.middleware.Cors`, verified agains

Matching is **exact string comparison** on the incoming `Origin` header. There is no wildcard-subdomain support: `https://*.myapp.com` is not a valid entry. Enumerate every origin you need — scheme + host + port, nothing more, nothing less.

<Aside type="caution">
**Migrating from 3.x global settings?** The `allowHeaders` default above (`"Content-Type,Authorization,X-Requested-With"`) is narrower than the legacy `accessControlAllowHeaders` global-setting default, which also included `X-Auth-Token`, `X-Requested-By`, and `Origin`. A like-for-like swap silently drops those headers. See [Upgrading from 3.x — CORS allow-list defaults drift](/v4-0-0/upgrading/3x-to-4x/#migrating-from-global-settings-to-the-middleware) for the side-by-side comparison and the explicit-constructor-args fix.
</Aside>

## Preflight (OPTIONS) requests

Before any non-simple cross-origin request (anything that uses a custom header, a non-GET/POST/HEAD method, or a non-simple content type), the browser sends a preflight `OPTIONS` request asking "may I?" The Cors middleware handles this for you:
Expand Down
25 changes: 25 additions & 0 deletions web/sites/guides/src/content/docs/v4-0-0/upgrading/3x-to-4x.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,30 @@ Unless you intentionally want this asymmetry, running both simultaneously is not
- Apps using the middleware: add `allowOrigins` explicitly (see the breaking change above).
- New CORS configuration: prefer the middleware — it supports per-route scoping and has a secure deny-all default.

#### Migrating from global settings to the middleware

If you follow the recommendation above and move from `set(accessControlAllow*)` global settings to the `wheels.middleware.Cors` constructor, be aware that the allow-list defaults are not a like-for-like match.

| Value | Legacy global setting default | `Cors` constructor default |
|---|---|---|
| Methods (`accessControlAllowMethods` / `allowMethods`) | `"GET, POST, PATCH, PUT, DELETE, OPTIONS"` | `"GET,POST,PUT,PATCH,DELETE,OPTIONS"` |
| Headers (`accessControlAllowHeaders` / `allowHeaders`) | `"Origin, Content-Type, X-Auth-Token, X-Requested-By, X-Requested-With"` | `"Content-Type,Authorization,X-Requested-With"` |

The middleware default drops `X-Auth-Token`, `X-Requested-By`, and `Origin` from the header allow-list. A like-for-like swap silently shrinks the list: preflight `OPTIONS` requests from clients that send those headers receive an `Access-Control-Allow-Headers` response that omits those headers, and the browser blocks the real request with no entry in your server logs.

Pass the missing headers explicitly when constructing the middleware:

```cfm {test:compile} title="config/settings.cfm"
set(middleware = [
new wheels.middleware.Cors(
allowOrigins = "https://myapp.com",
allowHeaders = "Content-Type,Authorization,X-Requested-With,X-Auth-Token,X-Requested-By,Origin"
)
]);
```

The methods difference (spaces vs no spaces between list items) is cosmetic — HTTP implementations trim list values. No action is needed unless you pattern-match the exact string.

### 2. HSTS defaults on in production

**CHANGELOG:** `Breaking: HSTS header defaults on in production` (#2081).
Expand Down Expand Up @@ -431,6 +455,7 @@ These are additive in 4.0 and worth adopting during the upgrade window:

- **"CSRF token invalid" on forms after upgrade.** Either the encryption key rotated (set `csrfEncryptionKey` explicitly) or a third-party embed is hitting `SameSite`. See #2035, #2054.
- **Requests return 403 with no CORS header.** `allowOrigins` isn't set. See #2039.
- **CORS preflight rejected after switching from global settings to the middleware.** The `Cors` constructor's `allowHeaders` default (`Content-Type,Authorization,X-Requested-With`) does not include `X-Auth-Token`, `X-Requested-By`, or `Origin` — all present in the legacy `accessControlAllowHeaders` global default. Pass the missing headers explicitly. See [Migrating from global settings to the middleware](#migrating-from-global-settings-to-the-middleware).
- **Rate limiter counts wrong behind your load balancer.** `trustProxy` defaults to `false` now. See #2024, #2088.
- **Test runner can't find specs.** Check for `tests/specs/functions/` — rename to `functional/`. See #1872.
- **Plugin warning at startup.** Port to a package (see above) or accept the warning until you do.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ These are the constructor arguments on `wheels.middleware.Cors`, verified agains

Matching is **exact string comparison** on the incoming `Origin` header. There is no wildcard-subdomain support: `https://*.myapp.com` is not a valid entry. Enumerate every origin you need — scheme + host + port, nothing more, nothing less.

<Aside type="caution">
**Migrating from 3.x global settings?** The `allowHeaders` default above (`"Content-Type,Authorization,X-Requested-With"`) is narrower than the legacy `accessControlAllowHeaders` global-setting default, which also included `X-Auth-Token`, `X-Requested-By`, and `Origin`. A like-for-like swap silently drops those headers. See [Upgrading from 3.x — CORS allow-list defaults drift](/v4-0-1-snapshot/upgrading/3x-to-4x/#migrating-from-global-settings-to-the-middleware) for the side-by-side comparison and the explicit-constructor-args fix.
</Aside>

## Preflight (OPTIONS) requests

Before any non-simple cross-origin request (anything that uses a custom header, a non-GET/POST/HEAD method, or a non-simple content type), the browser sends a preflight `OPTIONS` request asking "may I?" The Cors middleware handles this for you:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,30 @@ Unless you intentionally want this asymmetry, running both simultaneously is not
- Apps using the middleware: add `allowOrigins` explicitly (see the breaking change above).
- New CORS configuration: prefer the middleware — it supports per-route scoping and has a secure deny-all default.

#### Migrating from global settings to the middleware

If you follow the recommendation above and move from `set(accessControlAllow*)` global settings to the `wheels.middleware.Cors` constructor, be aware that the allow-list defaults are not a like-for-like match.

| Value | Legacy global setting default | `Cors` constructor default |
|---|---|---|
| Methods (`accessControlAllowMethods` / `allowMethods`) | `"GET, POST, PATCH, PUT, DELETE, OPTIONS"` | `"GET,POST,PUT,PATCH,DELETE,OPTIONS"` |
| Headers (`accessControlAllowHeaders` / `allowHeaders`) | `"Origin, Content-Type, X-Auth-Token, X-Requested-By, X-Requested-With"` | `"Content-Type,Authorization,X-Requested-With"` |

The middleware default drops `X-Auth-Token`, `X-Requested-By`, and `Origin` from the header allow-list. A like-for-like swap silently shrinks the list: preflight `OPTIONS` requests from clients that send those headers receive an `Access-Control-Allow-Headers` response that omits those headers, and the browser blocks the real request with no entry in your server logs.

Pass the missing headers explicitly when constructing the middleware:

```cfm {test:compile} title="config/settings.cfm"
set(middleware = [
new wheels.middleware.Cors(
allowOrigins = "https://myapp.com",
allowHeaders = "Content-Type,Authorization,X-Requested-With,X-Auth-Token,X-Requested-By,Origin"
)
]);
```

The methods difference (spaces vs no spaces between list items) is cosmetic — HTTP implementations trim list values. No action is needed unless you pattern-match the exact string.

### 2. HSTS defaults on in production

**CHANGELOG:** `Breaking: HSTS header defaults on in production` (#2081).
Expand Down Expand Up @@ -431,6 +455,7 @@ These are additive in 4.0 and worth adopting during the upgrade window:

- **"CSRF token invalid" on forms after upgrade.** Either the encryption key rotated (set `csrfEncryptionKey` explicitly) or a third-party embed is hitting `SameSite`. See #2035, #2054.
- **Requests return 403 with no CORS header.** `allowOrigins` isn't set. See #2039.
- **CORS preflight rejected after switching from global settings to the middleware.** The `Cors` constructor's `allowHeaders` default (`Content-Type,Authorization,X-Requested-With`) does not include `X-Auth-Token`, `X-Requested-By`, or `Origin` — all present in the legacy `accessControlAllowHeaders` global default. Pass the missing headers explicitly. See [Migrating from global settings to the middleware](#migrating-from-global-settings-to-the-middleware).
- **Rate limiter counts wrong behind your load balancer.** `trustProxy` defaults to `false` now. See #2024, #2088.
- **Test runner can't find specs.** Check for `tests/specs/functions/` — rename to `functional/`. See #1872.
- **Plugin warning at startup.** Port to a package (see above) or accept the warning until you do.
Expand Down
Binary file modified web/tests/visual-baselines/blog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading