openvibers/openvibe.coupons Commits
Coupons lookups that differ for a browser extension are never kept by the CDN, so an extension can never be handed a cached page-caller answer with DealNews codes in it (#80)
Changes
@@@ -55,9 +55,20 @@ function createApi(ctx) {return config.extensionOrigins.includes('moz-extension://*') && MOZ_EXTENSION_RE.test(origin);}++ /**++ * These answers differ for a browser extension (source-policy.js), and Cloudflare keys its cache on the URL alone++ * (it ignores Vary): the edge must never keep one, or an extension could be handed the page-caller's answer.++ * Browsers still cache them (Cache-Control, Vary: Origin).++ */++ function byOrigin(res) {++ res.vary('Origin');++ res.set('CDN-Cache-Control', 'no-store');++ res.set('Cloudflare-CDN-Cache-Control', 'no-store');++ }++/** CORS for the lookup routes only: exact configured extension origins, never credentials. */function lookupCors(req, res, next) {-- res.vary('Origin');++ byOrigin(res);const origin = req.get('origin');if (extensionOriginAllowed(origin)) {res.set('Access-Control-Allow-Origin', origin);@@@ -154,7 +165,7 @@ function createApi(ctx) {router.get('/merchants/:id', lookupLimiter, guard(CAPABILITIES.COUPON_LOOKUP, { scope: SCOPES.LOOKUP }), reads, run(async (req, res) => {const m = await activeMerchant(req.params.id);-- res.vary('Origin');++ byOrigin(res);publicCache(res);return { merchant: await merchantView(m, req) };}));@@@ -164,7 +175,7 @@ function createApi(ctx) {const m = c && await merchants.byId(c.merchant_id);if (!c || !m || m.status !== 'active' || c.review_state !== 'published' || c.status === 'disabled') throw new ApiError(404, 'coupon.not_found', 'no such code');const v = await coupons.view(c, { merchant: m });-- res.vary('Origin');++ byOrigin(res);if (v.extension_excluded && fromExtension(req)) throw new ApiError(404, 'coupon.not_found', 'no such code');publicCache(res);return { coupon: v, merchant: await merchantView(m, req) };@@@ -125,6 +125,7 @@ const { boot, check, done } = require('./helpers/boot');assert.ok(!ext.json().coupons.some((x) => x.code === 'UXBMXG9V') && ext.json().merchant.active_codes === 0, `an extension never does (${headers.origin})`);assert.strictEqual((await t.get(`/api/v1/coupons/${c.id}`, { headers })).status, 404);assert.match(ext.headers.get('vary') || '', /Origin/);++ assert.strictEqual(ext.headers.get('cloudflare-cdn-cache-control'), 'no-store', 'the edge never keeps an answer that differs by caller');}});
@@ -55,9 +55,20 @@ function createApi(ctx) {
@@ -55,9 +55,20 @@ function createApi(ctx) {
return config.extensionOrigins.includes('moz-extension://*') && MOZ_EXTENSION_RE.test(origin);
return config.extensionOrigins.includes('moz-extension://*') && MOZ_EXTENSION_RE.test(origin);
}
}
+ /**
+ * These answers differ for a browser extension (source-policy.js), and Cloudflare keys its cache on the URL alone
+ * (it ignores Vary): the edge must never keep one, or an extension could be handed the page-caller's answer.
+ * Browsers still cache them (Cache-Control, Vary: Origin).
+ */
+ function byOrigin(res) {
+ res.vary('Origin');
+ res.set('CDN-Cache-Control', 'no-store');
+ res.set('Cloudflare-CDN-Cache-Control', 'no-store');
+ }
+
/** CORS for the lookup routes only: exact configured extension origins, never credentials. */
/** CORS for the lookup routes only: exact configured extension origins, never credentials. */
function lookupCors(req, res, next) {
function lookupCors(req, res, next) {
- res.vary('Origin');
+ byOrigin(res);
const origin = req.get('origin');
const origin = req.get('origin');
if (extensionOriginAllowed(origin)) {
if (extensionOriginAllowed(origin)) {
res.set('Access-Control-Allow-Origin', origin);
res.set('Access-Control-Allow-Origin', origin);
@@ -154,7 +165,7 @@ function createApi(ctx) {
@@ -154,7 +165,7 @@ function createApi(ctx) {
router.get('/merchants/:id', lookupLimiter, guard(CAPABILITIES.COUPON_LOOKUP, { scope: SCOPES.LOOKUP }), reads, run(async (req, res) => {
router.get('/merchants/:id', lookupLimiter, guard(CAPABILITIES.COUPON_LOOKUP, { scope: SCOPES.LOOKUP }), reads, run(async (req, res) => {
const m = await activeMerchant(req.params.id);
const m = await activeMerchant(req.params.id);
- res.vary('Origin');
+ byOrigin(res);
publicCache(res);
publicCache(res);
return { merchant: await merchantView(m, req) };
return { merchant: await merchantView(m, req) };
}));
}));
@@ -164,7 +175,7 @@ function createApi(ctx) {
@@ -164,7 +175,7 @@ function createApi(ctx) {
const m = c && await merchants.byId(c.merchant_id);
const m = c && await merchants.byId(c.merchant_id);
if (!c || !m || m.status !== 'active' || c.review_state !== 'published' || c.status === 'disabled') throw new ApiError(404, 'coupon.not_found', 'no such code');
if (!c || !m || m.status !== 'active' || c.review_state !== 'published' || c.status === 'disabled') throw new ApiError(404, 'coupon.not_found', 'no such code');
const v = await coupons.view(c, { merchant: m });
const v = await coupons.view(c, { merchant: m });
- res.vary('Origin');
+ byOrigin(res);
if (v.extension_excluded && fromExtension(req)) throw new ApiError(404, 'coupon.not_found', 'no such code');
if (v.extension_excluded && fromExtension(req)) throw new ApiError(404, 'coupon.not_found', 'no such code');
publicCache(res);
publicCache(res);
return { coupon: v, merchant: await merchantView(m, req) };
return { coupon: v, merchant: await merchantView(m, req) };
@@ -125,6 +125,7 @@ const { boot, check, done } = require('./helpers/boot');
@@ -125,6 +125,7 @@ const { boot, check, done } = require('./helpers/boot');
assert.ok(!ext.json().coupons.some((x) => x.code === 'UXBMXG9V') && ext.json().merchant.active_codes === 0, `an extension never does (${headers.origin})`);
assert.ok(!ext.json().coupons.some((x) => x.code === 'UXBMXG9V') && ext.json().merchant.active_codes === 0, `an extension never does (${headers.origin})`);
assert.strictEqual((await t.get(`/api/v1/coupons/${c.id}`, { headers })).status, 404);
assert.strictEqual((await t.get(`/api/v1/coupons/${c.id}`, { headers })).status, 404);
assert.match(ext.headers.get('vary') || '', /Origin/);
assert.match(ext.headers.get('vary') || '', /Origin/);
+ assert.strictEqual(ext.headers.get('cloudflare-cdn-cache-control'), 'no-store', 'the edge never keeps an answer that differs by caller');
}
}
});
});