mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Merge pull request #522 from jason5ng32/fix/guard-query-rewrites
Fix(api): make guard query rewrites survive Express 5
This commit is contained in:
3 files changed
+64
-5
No files matched your search
@@ -126,6 +126,8 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`).
|
|||||||
- `requireValidReportId()` — `/api/report/:id` route param (22-char base64url).
|
- `requireValidReportId()` — `/api/report/:id` route param (22-char base64url).
|
||||||
|
|
||||||
New param shape → new guard there, attached in `backend-server.js`.
|
New param shape → new guard there, attached in `backend-server.js`.
|
||||||
|
Rewrites go through `setQueryParam()` there: Express 5's `req.query` re-parses
|
||||||
|
on every read, so a direct write is lost.
|
||||||
|
|
||||||
### Response enrichment lives in middleware too
|
### Response enrichment lives in middleware too
|
||||||
|
|
||||||
|
|||||||
+17
-5
@@ -11,6 +11,18 @@ import { STATUS_PROVIDER_IDS } from './service-status-providers.js';
|
|||||||
import { DNS_RECORD_TYPE_SET } from '../common/dns-record-types.js';
|
import { DNS_RECORD_TYPE_SET } from '../common/dns-record-types.js';
|
||||||
import { parseAsnInput } from '../common/asn-input.js';
|
import { parseAsnInput } from '../common/asn-input.js';
|
||||||
|
|
||||||
|
// Express 5's req.query is a prototype getter that re-parses the URL on every
|
||||||
|
// read, so a direct write is lost. Pin the parsed object as an own property
|
||||||
|
// first; later guards, cacheable resolvers and the handler then share it.
|
||||||
|
const setQueryParam = (req, name, value) => {
|
||||||
|
if (!Object.hasOwn(req, 'query')) {
|
||||||
|
Object.defineProperty(req, 'query', {
|
||||||
|
value: req.query, writable: true, configurable: true, enumerable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
req.query[name] = value;
|
||||||
|
};
|
||||||
|
|
||||||
// Reject requests without an allowed referer. The error message variant
|
// Reject requests without an allowed referer. The error message variant
|
||||||
// preserves the existing user-facing wording.
|
// preserves the existing user-facing wording.
|
||||||
export const requireReferer = (req, res, next) => {
|
export const requireReferer = (req, res, next) => {
|
||||||
@@ -57,7 +69,7 @@ export const requireValidDomain = (paramName = 'domain') => (req, res, next) =>
|
|||||||
if (!isValidDomain(domain)) {
|
if (!isValidDomain(domain)) {
|
||||||
return res.status(400).json({ error: 'Invalid domain' });
|
return res.status(400).json({ error: 'Invalid domain' });
|
||||||
}
|
}
|
||||||
req.query[paramName] = domain;
|
setQueryParam(req, paramName, domain);
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,7 +101,7 @@ export const requireValidASN = (paramName = 'asn') => (req, res, next) => {
|
|||||||
if (asn === null) {
|
if (asn === null) {
|
||||||
return res.status(400).json({ error: 'Invalid ASN' });
|
return res.status(400).json({ error: 'Invalid ASN' });
|
||||||
}
|
}
|
||||||
req.query[paramName] = String(asn);
|
setQueryParam(req, paramName, String(asn));
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,7 +116,7 @@ export const normalizeAsnQuery = (paramName = 'q') => (req, res, next) => {
|
|||||||
if (asn === null) {
|
if (asn === null) {
|
||||||
return res.status(400).json({ error: 'Invalid ASN' });
|
return res.status(400).json({ error: 'Invalid ASN' });
|
||||||
}
|
}
|
||||||
req.query[paramName] = `AS${asn}`;
|
setQueryParam(req, paramName, `AS${asn}`);
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,7 +131,7 @@ export const requireValidCountry = (paramName = 'country') => (req, res, next) =
|
|||||||
if (!/^[A-Za-z]{2}$/.test(raw)) {
|
if (!/^[A-Za-z]{2}$/.test(raw)) {
|
||||||
return res.status(400).json({ error: 'Invalid country' });
|
return res.status(400).json({ error: 'Invalid country' });
|
||||||
}
|
}
|
||||||
req.query[paramName] = String(raw).toUpperCase();
|
setQueryParam(req, paramName, String(raw).toUpperCase());
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,7 +157,7 @@ export const requireValidRecordType = (paramName = 'type') => (req, res, next) =
|
|||||||
if (!DNS_RECORD_TYPE_SET.has(String(type).toUpperCase())) {
|
if (!DNS_RECORD_TYPE_SET.has(String(type).toUpperCase())) {
|
||||||
return res.status(400).json({ error: 'Invalid record type' });
|
return res.status(400).json({ error: 'Invalid record type' });
|
||||||
}
|
}
|
||||||
req.query[paramName] = String(type).toUpperCase();
|
setQueryParam(req, paramName, String(type).toUpperCase());
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { describe, it } from 'node:test';
|
import { describe, it } from 'node:test';
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
import { requireReferer, requirePublicIP, requireValidPrefix, requireValidDomain, requireValidProviderId, requireValidRecordType, requireValidReportId, requireValidCountry, requireValidASN, normalizeAsnQuery } from '../server/guards.js';
|
import { requireReferer, requirePublicIP, requireValidPrefix, requireValidDomain, requireValidProviderId, requireValidRecordType, requireValidReportId, requireValidCountry, requireValidASN, normalizeAsnQuery } from '../server/guards.js';
|
||||||
|
|
||||||
@@ -407,3 +408,47 @@ describe('normalizeAsnQuery', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Express 5 defines req.query as a prototype getter that re-parses the URL
|
||||||
|
// on every read; a real request (never listening) catches rewrites that a
|
||||||
|
// plain-object stub would keep.
|
||||||
|
describe('guard rewrites on a real Express request', () => {
|
||||||
|
const app = express();
|
||||||
|
const makeExpressReq = (url) => {
|
||||||
|
const req = Object.create(app.request);
|
||||||
|
req.url = url;
|
||||||
|
req.headers = {};
|
||||||
|
return req;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('reproduces the re-parsing getter', () => {
|
||||||
|
const req = makeExpressReq('/x?type=caa');
|
||||||
|
assert.notEqual(req.query, req.query);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [name, guard, url, param, expected] of [
|
||||||
|
['requireValidRecordType', requireValidRecordType(), '/x?type=caa', 'type', 'CAA'],
|
||||||
|
['requireValidDomain', requireValidDomain(), '/x?domain=Example.COM', 'domain', 'example.com'],
|
||||||
|
['requireValidASN', requireValidASN(), '/x?asn=AS013335', 'asn', '13335'],
|
||||||
|
['normalizeAsnQuery', normalizeAsnQuery(), '/x?q=as13335', 'q', 'AS13335'],
|
||||||
|
['requireValidCountry', requireValidCountry(), '/x?country=us', 'country', 'US'],
|
||||||
|
]) {
|
||||||
|
it(`${name} rewrite survives a later req.query read`, () => {
|
||||||
|
const req = makeExpressReq(url);
|
||||||
|
let nextCalled = false;
|
||||||
|
guard(req, makeRes(), () => { nextCalled = true; });
|
||||||
|
assert.equal(nextCalled, true);
|
||||||
|
assert.equal(req.query[param], expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('keeps both rewrites when chained like /api/dnsresolver', () => {
|
||||||
|
const req = makeExpressReq('/x?type=caa&hostname=Example.COM');
|
||||||
|
let calls = 0;
|
||||||
|
requireValidDomain('hostname')(req, makeRes(), () => { calls++; });
|
||||||
|
requireValidRecordType()(req, makeRes(), () => { calls++; });
|
||||||
|
assert.equal(calls, 2);
|
||||||
|
assert.deepEqual({ ...req.query }, { type: 'CAA', hostname: 'example.com' });
|
||||||
|
assert.equal(req.query, req.query);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user