fix: allow private lan cors origins
This commit is contained in:
parent
1c081038dd
commit
5ad3c46836
@ -24,7 +24,7 @@ If anything conflicts, follow **this** doc.
|
||||
- `docker compose -f docker-compose.dev.yml up -d --build backend`
|
||||
- After backend env/CORS changes, recreate the backend service so `backend/.env` is reloaded:
|
||||
- `docker compose -f docker-compose.dev.yml up -d --force-recreate --no-deps backend`
|
||||
- For the Docker frontend on port `3010`, `ALLOWED_ORIGINS` must include the exact browser origin, for example `http://localhost:3010` and `http://127.0.0.1:3010`.
|
||||
- For the Docker frontend on port `3010`, `ALLOWED_ORIGINS` must include exact loopback browser origins such as `http://localhost:3010` and `http://127.0.0.1:3010`; private LAN origins such as `http://192.168.7.45:3010` are allowed by the backend CORS helper for local device testing.
|
||||
- Verify the restarted API with `GET http://127.0.0.1:5000/` and `GET http://127.0.0.1:5000/config`.
|
||||
- Do not print or commit real `.env` values while checking or updating local Docker env.
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ const path = require("path");
|
||||
const User = require("./models/user.model");
|
||||
const requestIdMiddleware = require("./middleware/request-id");
|
||||
const { sendError } = require("./utils/http");
|
||||
const { isAllowedCorsOrigin, parseAllowedOrigins } = require("./utils/cors-origins");
|
||||
|
||||
const app = express();
|
||||
app.use(requestIdMiddleware);
|
||||
@ -14,17 +15,11 @@ if (process.env.NODE_ENV !== "production") {
|
||||
app.use("/test", express.static(path.join(__dirname, "public")));
|
||||
}
|
||||
|
||||
const allowedOrigins = (process.env.ALLOWED_ORIGINS || "")
|
||||
.split(",")
|
||||
.map((origin) => origin.trim())
|
||||
.filter(Boolean);
|
||||
const allowedOrigins = parseAllowedOrigins(process.env.ALLOWED_ORIGINS || "");
|
||||
app.use(
|
||||
cors({
|
||||
origin: function (origin, callback) {
|
||||
if (!origin) return callback(null, true);
|
||||
if (allowedOrigins.includes(origin)) return callback(null, true);
|
||||
if (/^http:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true);
|
||||
if (/^https:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true);
|
||||
if (isAllowedCorsOrigin(origin, allowedOrigins)) return callback(null, true);
|
||||
console.error(`CORS blocked origin: ${origin}`);
|
||||
callback(new Error(`CORS blocked: ${origin}. Add this origin to ALLOWED_ORIGINS environment variable.`));
|
||||
},
|
||||
|
||||
37
backend/tests/cors-origins.test.js
Normal file
37
backend/tests/cors-origins.test.js
Normal file
@ -0,0 +1,37 @@
|
||||
const {
|
||||
isAllowedCorsOrigin,
|
||||
isPrivateLanHostname,
|
||||
parseAllowedOrigins,
|
||||
} = require("../utils/cors-origins");
|
||||
|
||||
describe("CORS origin helpers", () => {
|
||||
test("parses configured allowed origins", () => {
|
||||
expect(parseAllowedOrigins(" http://localhost:3010, http://127.0.0.1:3010 ,,")).toEqual([
|
||||
"http://localhost:3010",
|
||||
"http://127.0.0.1:3010",
|
||||
]);
|
||||
});
|
||||
|
||||
test("allows exact configured origins", () => {
|
||||
const allowedOrigins = ["http://localhost:3010"];
|
||||
|
||||
expect(isAllowedCorsOrigin("http://localhost:3010", allowedOrigins)).toBe(true);
|
||||
});
|
||||
|
||||
test("allows browser access from the local private network", () => {
|
||||
expect(isAllowedCorsOrigin("http://192.168.7.45:3010", [])).toBe(true);
|
||||
});
|
||||
|
||||
test("recognizes RFC1918 private IPv4 hostnames", () => {
|
||||
expect(isPrivateLanHostname("10.1.2.3")).toBe(true);
|
||||
expect(isPrivateLanHostname("172.16.0.1")).toBe(true);
|
||||
expect(isPrivateLanHostname("172.31.255.254")).toBe(true);
|
||||
expect(isPrivateLanHostname("192.168.7.45")).toBe(true);
|
||||
});
|
||||
|
||||
test("rejects public and lookalike hostnames", () => {
|
||||
expect(isAllowedCorsOrigin("http://192.168.7.45.example.test:3010", [])).toBe(false);
|
||||
expect(isAllowedCorsOrigin("http://203.0.113.10:3010", [])).toBe(false);
|
||||
expect(isAllowedCorsOrigin("ftp://192.168.7.45:3010", [])).toBe(false);
|
||||
});
|
||||
});
|
||||
54
backend/utils/cors-origins.js
Normal file
54
backend/utils/cors-origins.js
Normal file
@ -0,0 +1,54 @@
|
||||
function parseAllowedOrigins(value = "") {
|
||||
return String(value)
|
||||
.split(",")
|
||||
.map((origin) => origin.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parseIpv4(hostname) {
|
||||
const parts = hostname.split(".");
|
||||
if (parts.length !== 4) return null;
|
||||
|
||||
const octets = parts.map((part) => {
|
||||
if (!/^\d{1,3}$/.test(part)) return null;
|
||||
|
||||
const value = Number(part);
|
||||
return value >= 0 && value <= 255 ? value : null;
|
||||
});
|
||||
|
||||
return octets.some((octet) => octet === null) ? null : octets;
|
||||
}
|
||||
|
||||
function isPrivateLanHostname(hostname) {
|
||||
const octets = parseIpv4(hostname);
|
||||
if (!octets) return false;
|
||||
|
||||
const [first, second] = octets;
|
||||
return (
|
||||
first === 10 ||
|
||||
(first === 172 && second >= 16 && second <= 31) ||
|
||||
(first === 192 && second === 168)
|
||||
);
|
||||
}
|
||||
|
||||
function isAllowedCorsOrigin(origin, allowedOrigins = []) {
|
||||
if (!origin) return true;
|
||||
if (allowedOrigins.includes(origin)) return true;
|
||||
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isPrivateLanHostname(url.hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isAllowedCorsOrigin,
|
||||
isPrivateLanHostname,
|
||||
parseAllowedOrigins,
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user