Florent 2 weeks ago committed by GitHub
commit c23778c395
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -299,7 +299,6 @@ Grist can be configured in many ways. Here are the main environment variables it
| GRIST_SNAPSHOT_TIME_CAP | optional. Define the caps for tracking buckets. Usage: {"hour": 25, "day": 32, "isoWeek": 12, "month": 96, "year": 1000} |
| GRIST_SNAPSHOT_KEEP | optional. Number of recent snapshots to retain unconditionally for a document, regardless of when they were made |
| GRIST_PROMCLIENT_PORT | optional. If set, serve the Prometheus metrics on the specified port number. ⚠️ Be sure to use a port which is not publicly exposed ⚠️. |
#### AI Formula Assistant related variables (all optional):
Variable | Purpose

@ -18,7 +18,7 @@ import {makeId} from 'app/server/lib/idUtils';
import log from 'app/server/lib/log';
import {IPermitStore, Permit} from 'app/server/lib/Permit';
import {AccessTokenInfo} from 'app/server/lib/AccessTokens';
import {allowHost, getOriginUrl, optStringParam} from 'app/server/lib/requestUtils';
import {allowHost, getOriginUrl, isEnvironmentAllowedHost, optStringParam} from 'app/server/lib/requestUtils';
import * as cookie from 'cookie';
import {NextFunction, Request, RequestHandler, Response} from 'express';
import {IncomingMessage} from 'http';
@ -271,7 +271,7 @@ export async function addRequestUser(
// custom-domain owner could hijack such sessions.
const allowedOrg = getAllowedOrgForSessionID(mreq.sessionID);
if (allowedOrg) {
if (allowHost(req, allowedOrg.host)) {
if (allowHost(req, allowedOrg.host) || isEnvironmentAllowedHost(allowedOrg.host)) {
customHostSession = ` custom-host-match ${allowedOrg.host}`;
} else {
// We need an exception for internal forwarding from home server to doc-workers. These use

@ -9,6 +9,7 @@ import log from 'app/server/lib/log';
import {Permit} from 'app/server/lib/Permit';
import {Request, Response} from 'express';
import { IncomingMessage } from 'http';
import _ from 'lodash';
import {Writable} from 'stream';
import { TLSSocket } from 'tls';
@ -87,7 +88,7 @@ export function trustOrigin(req: IncomingMessage, resp?: Response): boolean {
// Note that the request origin is undefined for non-CORS requests.
const origin = req.headers.origin;
if (!origin) { return true; } // Not a CORS request.
if (!allowHost(req, new URL(origin))) { return false; }
if (!allowHost(req, new URL(origin)) && !isEnvironmentAllowedHost(new URL(origin))) { return false; }
if (resp) {
// For a request to a custom domain, the full hostname must match.
@ -111,14 +112,14 @@ export function allowHost(req: IncomingMessage, allowedHost: string|URL) {
});
if ((req as RequestWithOrg).isCustomHost) {
// For a request to a custom domain, the full hostname must match.
return actualUrl.hostname === allowedUrl.hostname;
return actualUrl.hostname === allowedUrl.hostname;
} else {
// For requests to a native subdomains, only the base domain needs to match.
const allowedDomain = parseSubdomain(allowedUrl.hostname);
const actualDomain = parseSubdomain(actualUrl.hostname);
return actualDomain.base ?
return (!_.isEmpty(actualDomain) ?
actualDomain.base === allowedDomain.base :
actualUrl.hostname === allowedUrl.hostname;
allowedUrl.hostname === actualUrl.hostname);
}
}
@ -126,6 +127,13 @@ export function matchesBaseDomain(domain: string, baseDomain: string) {
return domain === baseDomain || domain.endsWith("." + baseDomain);
}
export function isEnvironmentAllowedHost(url: string|URL) {
const urlHost = (typeof url === 'string') ? url : url.hostname;
return (process.env.GRIST_ALLOWED_HOSTS || "").split(",").some(domain =>
domain && matchesBaseDomain(urlHost, domain)
);
}
export function isParameterOn(parameter: any): boolean {
return gutil.isAffirmative(parameter);
}

@ -4965,6 +4965,23 @@ function testDocApi() {
});
describe("Allowed Origin", () => {
it('should allow only example.com', async () => {
async function checkOrigin(origin: string, allowed: boolean) {
const resp = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/data`,
{...chimpy, headers: {...chimpy.headers, "Origin": origin}}
);
assert.equal(resp.headers['access-control-allow-credentials'], allowed ? 'true' : undefined);
assert.equal(resp.status, allowed ? 200 : 403);
}
await checkOrigin("https://www.toto.com", false);
await checkOrigin("https://badexample.com", false);
await checkOrigin("https://bad.com/example.com/toto", false);
await checkOrigin("https://example.com/path", true);
await checkOrigin("https://example.com:3000/path", true);
await checkOrigin("https://good.example.com/toto", true);
});
it("should respond with correct CORS headers", async function () {
const wid = await getWorkspaceId(userApi, 'Private');
const docId = await userApi.newDoc({name: 'CorsTestDoc'}, wid);

@ -49,6 +49,7 @@ export class TestServer {
GRIST_PORT: '0',
GRIST_DISABLE_S3: 'true',
REDIS_URL: process.env.TEST_REDIS_URL,
GRIST_ALLOWED_HOSTS: `example.com,localhost`,
GRIST_TRIGGER_WAIT_DELAY: '100',
// this is calculated value, some tests expect 4 attempts and some will try 3 times
GRIST_TRIGGER_MAX_ATTEMPTS: '4',

Loading…
Cancel
Save