@blamejs/blamejs-shop 0.5.6 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/storefront.js +30 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.5.x
|
|
10
10
|
|
|
11
|
+
- v0.5.7 (2026-07-03) — **Storefront redirect targets route through the framework open-redirect guard, closing a TAB-injection open redirect on return_to / redirect_to.** Closes an open-redirect (CWE-601) in the storefront's post-action redirects. The cookie-consent submit, currency and locale switchers, and the wishlist and compare toggles validated their return_to / redirect_to target with a hand-rolled leading-slash regex that accepted a horizontal TAB (0x09) at the second byte. A user agent strips ASCII TAB, CR, and LF from a URL before resolving it, so a target like /<TAB>/evil.example collapsed to the protocol-relative //evil.example and navigated the shopper off-origin under the shop's trust — a phishing bounce. Every request-derived redirect target now routes through the framework's b.safeRedirect.resolve primitive (exposed once as a shared same-origin helper), which rejects every control character including TAB, protocol-relative //host and \\host forms, and full URLs — only genuine same-origin relative paths survive; anything else falls back to a safe default. The two guards that already rejected control characters are consolidated onto the same primitive so there is one redirect-safety path to reason about, and a codebase check refuses any future hand-rolled leading-slash redirect guard. No operator action and no configuration change. **Security:** *Open-redirect on storefront return_to / redirect_to closed* — The post-action redirect guards (cookie consent, currency switch, locale switch, wishlist toggle, compare toggle, announcement dismiss) accepted a leading-slash target whose second byte was a control character. A horizontal TAB there is stripped by the browser before URL parsing, turning /<TAB>/evil.example into the protocol-relative //evil.example — an off-origin redirect an attacker could use to launder a phishing link through the shop's origin. All these targets now resolve through b.safeRedirect.resolve, which rejects control characters (TAB, CR, LF, NUL), protocol-relative and backslash forms, and off-origin absolute URLs; an invalid target falls back to a same-origin default. A codebase-patterns check prevents a hand-rolled leading-slash redirect guard from reappearing.
|
|
12
|
+
|
|
11
13
|
- v0.5.6 (2026-07-03) — **Minimum Node is now 24.18.0; the vendored framework refresh brings a broad batch of request-path and mail-authentication security hardening.** Refreshes the vendored framework to 0.16.2 and raises the minimum supported Node.js to 24.18.0 — operators must run Node 24.18.0 or newer before upgrading. The floor moves with the framework, which requires 24.18.0 for its bundled SQLite 3.53.1, a backup-path fix, and the native OpenSSL 3.5 SLH-DSA surface. The refresh carries a large batch of security and correctness fixes that harden the request lifecycle the storefront and account portal already run on, at no code cost to the shop: the router refuses an encoded path separator or null byte and decodes the request path once, so a path-scoped security gate and the resource it guards can no longer disagree; res.redirect rejects a horizontal TAB that user agents strip before URL parsing, closing an open-redirect bypass; OAuth refresh-token and step-up replay gates treat any truthy store result as seen and bind grants to the authenticated principal; SAML Single-Logout fails closed when a verification key arrives without its algorithm; the mail-authentication builders (Authentication-Results, delivery-status, ARC) refuse CR/LF/NUL header injection and SPF evaluation fails closed on a malformed ip4 CIDR; file uploads enforce per-upload ownership and reject path-traversal ids; recursive parsers refuse pathologically deep input instead of overflowing the stack; and shared-cache counters (lockout, byte quotas, rate caps) accumulate with an atomic compare-and-set so distributed requests cannot lose increments. Every Node-version pin — engines, .nvmrc, the container base image, CI, and the docs — is synced to the new floor from the framework's own engines constraint. **Changed:** *Minimum Node.js raised to 24.18.0* — engines.node is now >=24.18.0 (was >=24.16.0). Operators must run Node 24.18.0 or newer. The floor is dictated by the vendored framework, which requires 24.18.0 for its bundled SQLite 3.53.1, a database backup-path fix, and the native OpenSSL 3.5 SLH-DSA sign/verify surface. The .nvmrc, the container base image, the CI runner pins, and the README/architecture docs are all synced to the new floor. · *Vendored framework refreshed to 0.16.2* — The framework the shop builds on is refreshed from 0.15.38 to 0.16.2. Beyond the request-path and mail hardening in this release's summary, the refresh includes: the router rejects percent-encoded path separators so a security middleware and the route it guards agree on the path; ARC and DKIM verification reject forged or body-length-truncated signatures; OCSP validation binds the response to the certificate's issuer; status-list verification binds the token type against replay; and OAuth authorization-URL builders refuse operator parameters that collide with framework-managed ones. These land in the request lifecycle the storefront and account portal already compose, with no shop code change required.
|
|
12
14
|
|
|
13
15
|
- v0.5.5 (2026-06-27) — **Customers can change their subscription plan — proration-aware upgrades and downgrades from the account portal.** Adds a self-serve plan-change surface to the customer subscription portal. From /account/subscriptions a customer can switch an active subscription to any other plan in the same currency: the page previews the proration for each candidate plan and offers two timings. An immediate change applies now and prorates the rest of the current period — an upgrade is charged the prorated difference (recorded on the subscription invoice ledger), and a downgrade is issued the unused remainder as store credit. A next-billing-cycle change is queued and applied automatically when the period rolls over, with the new plan starting clean and no proration. The transition is exactly-once: the immediate path claims the plan switch atomically before recording the charge or credit, and the scheduled path claims each due change with a single guarded update, so a retry or an overlapping scheduler run cannot double-charge or double-credit. A downgrade that owes a credit is never applied without paying it — if the credit write fails the change rolls back and retries. The surface mounts automatically when subscriptions and store credit are composed (the default for a configured storefront); the scheduler runs on the existing cron, and the tables it uses already shipped, so there are no schema changes and no operator action is required. **Added:** *Self-serve subscription plan changes with proration* — Customers can change the plan on an active subscription from /account/subscriptions/:id/change. The page lists every plan in the subscription's currency (excluding the current and archived plans) with a proration preview, and offers an immediate switch or a switch at the next billing date. An immediate upgrade charges the prorated difference for the rest of the period and records it on the subscription invoice ledger; an immediate downgrade issues the unused remainder of the outgoing plan as store credit. A still-pending change is shown on the subscription with a one-click cancel. Cross-currency, same-plan, and archived-plan requests are refused server-side, and the surface enforces subscription ownership before any read or write. · *Scheduler applies next-billing-cycle changes automatically* — A change queued for the next billing cycle is applied without operator involvement: a minute-cadence tick flips every due change to its new plan, records the proration, and issues any owed credit. Each due change is claimed with a single guarded update so an overlapping or retried run cannot apply it twice, and a change that owes a credit is left queued rather than applied if no credit can be issued — the credit is never dropped. The scheduler is inert on a deploy without subscriptions or store credit composed. **Changed:** *Dunning step failures are logged with their cause* — When a dunning step (reminder send, retry-charge record, pause, or cancel) fails, the schedule still advances and the step is recorded as failed — but the underlying error is now written to the server log instead of being discarded, so an operator can see why a step failed rather than only that it did.
|
package/lib/asset-manifest.json
CHANGED
package/lib/storefront.js
CHANGED
|
@@ -59,6 +59,25 @@ var _localeAls = new AsyncLocalStorage();
|
|
|
59
59
|
// the client only ever sees a generic message + the correlating id.
|
|
60
60
|
var _log = b.log.create({});
|
|
61
61
|
|
|
62
|
+
// Same-origin redirect-target guard shared by every post-action redirect
|
|
63
|
+
// (return_to / redirect_to / next). Delegates to the framework open-redirect
|
|
64
|
+
// (CWE-601) primitive rather than a hand-rolled path regex: it rejects
|
|
65
|
+
// protocol-relative targets ("//host", "\\host"), refuses full URLs (no
|
|
66
|
+
// base/allowlist is passed, so only same-origin relatives survive), and
|
|
67
|
+
// rejects EVERY control char including a horizontal TAB. A hand-rolled
|
|
68
|
+
// `/^\/[^/]/` guard misses the TAB: a user agent strips ASCII TAB/CR/LF from
|
|
69
|
+
// a URL before resolving it, so a `Location: /\t/evil.example` collapses to
|
|
70
|
+
// the protocol-relative `//evil.example` and navigates off-origin. Returns
|
|
71
|
+
// the safe same-origin path (query/fragment preserved) or `fallback` — pass
|
|
72
|
+
// "" as the fallback to use it as an accepted/rejected sentinel.
|
|
73
|
+
function _sameOriginRedirect(raw, fallback) {
|
|
74
|
+
var fb = typeof fallback === "string" ? fallback : "/";
|
|
75
|
+
// A pathological over-length target (header bloat) falls back before the
|
|
76
|
+
// primitive even parses it.
|
|
77
|
+
if (typeof raw === "string" && raw.length > 2048) return fb;
|
|
78
|
+
return b.safeRedirect.resolve(raw, { fallback: fb });
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
// Generic 500 for an auth/ceremony route: log the real error server-side
|
|
63
82
|
// (correlated by the framework request id) and return a fixed message to
|
|
64
83
|
// the client so no internal error string (stack frame, DB column, vault
|
|
@@ -12485,10 +12504,7 @@ function mount(router, deps) {
|
|
|
12485
12504
|
router.post("/announcements/:slug/dismiss", async function (req, res) {
|
|
12486
12505
|
var slug = (req.params && typeof req.params.slug === "string") ? req.params.slug : "";
|
|
12487
12506
|
var body = req.body || {};
|
|
12488
|
-
var to = (
|
|
12489
|
-
if (to.charAt(0) !== "/" || to.charAt(1) === "/" || to.indexOf("\\") !== -1 || /[\x00-\x1f\x7f]/.test(to)) {
|
|
12490
|
-
to = "/";
|
|
12491
|
-
}
|
|
12507
|
+
var to = _sameOriginRedirect(body.return_to, "/");
|
|
12492
12508
|
if (/^[a-z0-9-]{1,64}$/.test(slug)) {
|
|
12493
12509
|
// Durable record (best-effort — keyed on the cart session id; absent
|
|
12494
12510
|
// a session the cookie still carries the dismissal for this browser).
|
|
@@ -13125,13 +13141,9 @@ function mount(router, deps) {
|
|
|
13125
13141
|
// setting a cookie, so a hostile link can't 500 the route.
|
|
13126
13142
|
router.get("/locale", function (req, res) {
|
|
13127
13143
|
var q = req.query || {};
|
|
13128
|
-
|
|
13129
|
-
//
|
|
13130
|
-
|
|
13131
|
-
// back to the home path.
|
|
13132
|
-
if (to.charAt(0) !== "/" || to.charAt(1) === "/" || to.indexOf("\\") !== -1 || /[\x00-\x1f\x7f]/.test(to)) {
|
|
13133
|
-
to = "/";
|
|
13134
|
-
}
|
|
13144
|
+
// Same-origin path only (rejects protocol-relative "//", backslash, and
|
|
13145
|
+
// control chars including TAB); anything else falls back to home.
|
|
13146
|
+
var to = _sameOriginRedirect(q.to, "/");
|
|
13135
13147
|
var lang = (typeof q.lang === "string") ? q.lang : "";
|
|
13136
13148
|
if (lang && LOCALE_COOKIE_RE.test(lang) && lang.length <= LOCALE_COOKIE_MAX && supportedTags.indexOf(lang) !== -1) {
|
|
13137
13149
|
_setLocaleCookie(res, lang);
|
|
@@ -13898,7 +13910,7 @@ function mount(router, deps) {
|
|
|
13898
13910
|
// slash, no scheme / host / protocol-relative `//`) so the switcher
|
|
13899
13911
|
// can't be turned into an open redirect.
|
|
13900
13912
|
var rawTo = typeof body.redirect_to === "string" ? body.redirect_to : "/";
|
|
13901
|
-
var to = (
|
|
13913
|
+
var to = _sameOriginRedirect(rawTo, "/");
|
|
13902
13914
|
if (chosen === _ccyBase || _ccyOptions.indexOf(chosen) === -1) {
|
|
13903
13915
|
_clearCurrencyCookie(res);
|
|
13904
13916
|
} else {
|
|
@@ -14445,7 +14457,8 @@ function mount(router, deps) {
|
|
|
14445
14457
|
// failure still lands the shopper somewhere sane.
|
|
14446
14458
|
var rt = (req.body || {}).return_to;
|
|
14447
14459
|
var dest = null;
|
|
14448
|
-
|
|
14460
|
+
var safeRt = _sameOriginRedirect(rt, "");
|
|
14461
|
+
if (safeRt) dest = safeRt;
|
|
14449
14462
|
|
|
14450
14463
|
var notice;
|
|
14451
14464
|
try {
|
|
@@ -17801,10 +17814,11 @@ function mount(router, deps) {
|
|
|
17801
17814
|
var okKind = removed ? "removed" : "added";
|
|
17802
17815
|
var rt = (req.body || {}).return_to;
|
|
17803
17816
|
var dest;
|
|
17804
|
-
|
|
17817
|
+
var safeRt = _sameOriginRedirect(rt, "");
|
|
17818
|
+
if (safeRt) {
|
|
17805
17819
|
// Only thread the success marker when returning to the wishlist
|
|
17806
17820
|
// page (the PDP heart toggle is its own visible cue).
|
|
17807
|
-
dest =
|
|
17821
|
+
dest = safeRt.indexOf("/account/wishlist") === 0 ? "/account/wishlist?ok=" + okKind : safeRt;
|
|
17808
17822
|
} else {
|
|
17809
17823
|
var product = null;
|
|
17810
17824
|
try { product = await deps.catalog.products.get(productId); } catch (_e) { product = null; }
|
|
@@ -22131,8 +22145,7 @@ function mount(router, deps) {
|
|
|
22131
22145
|
// refused), capping length defensively. Anything else collapses to the
|
|
22132
22146
|
// safe default the caller passes.
|
|
22133
22147
|
function _consentReturnTo(raw, fallback) {
|
|
22134
|
-
|
|
22135
|
-
return fallback;
|
|
22148
|
+
return _sameOriginRedirect(raw, fallback);
|
|
22136
22149
|
}
|
|
22137
22150
|
|
|
22138
22151
|
// Translate a posted choice + per-category checkboxes into the four
|
package/package.json
CHANGED