mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) updates from grist-core
This commit is contained in:
commit
433e1ecfc2
@ -202,6 +202,7 @@ APP_STATIC_URL | url prefix for static resources
|
|||||||
APP_STATIC_INCLUDE_CUSTOM_CSS | set to "true" to include custom.css (from APP_STATIC_URL) in static pages
|
APP_STATIC_INCLUDE_CUSTOM_CSS | set to "true" to include custom.css (from APP_STATIC_URL) in static pages
|
||||||
APP_UNTRUSTED_URL | URL at which to serve/expect plugin content.
|
APP_UNTRUSTED_URL | URL at which to serve/expect plugin content.
|
||||||
GRIST_ADAPT_DOMAIN | set to "true" to support multiple base domains (careful, host header should be trustworthy)
|
GRIST_ADAPT_DOMAIN | set to "true" to support multiple base domains (careful, host header should be trustworthy)
|
||||||
|
GRIST_ALLOWED_HOSTS | comma-separated list of permitted domains origin for requests (e.g. my.site,another.com)
|
||||||
GRIST_APP_ROOT | directory containing Grist sandbox and assets (specifically the sandbox and static subdirectories).
|
GRIST_APP_ROOT | directory containing Grist sandbox and assets (specifically the sandbox and static subdirectories).
|
||||||
GRIST_BACKUP_DELAY_SECS | wait this long after a doc change before making a backup
|
GRIST_BACKUP_DELAY_SECS | wait this long after a doc change before making a backup
|
||||||
GRIST_DATA_DIR | directory in which to store document caches.
|
GRIST_DATA_DIR | directory in which to store document caches.
|
||||||
|
@ -18,7 +18,7 @@ import {makeId} from 'app/server/lib/idUtils';
|
|||||||
import log from 'app/server/lib/log';
|
import log from 'app/server/lib/log';
|
||||||
import {IPermitStore, Permit} from 'app/server/lib/Permit';
|
import {IPermitStore, Permit} from 'app/server/lib/Permit';
|
||||||
import {AccessTokenInfo} from 'app/server/lib/AccessTokens';
|
import {AccessTokenInfo} from 'app/server/lib/AccessTokens';
|
||||||
import {allowHost, getOriginUrl, optStringParam} from 'app/server/lib/requestUtils';
|
import {allowHost, isEnvironmentAllowedHost, getOriginUrl, optStringParam} from 'app/server/lib/requestUtils';
|
||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
import {NextFunction, Request, RequestHandler, Response} from 'express';
|
import {NextFunction, Request, RequestHandler, Response} from 'express';
|
||||||
import {IncomingMessage} from 'http';
|
import {IncomingMessage} from 'http';
|
||||||
@ -263,7 +263,7 @@ export async function addRequestUser(dbManager: HomeDBManager, permitStore: IPer
|
|||||||
// custom-domain owner could hijack such sessions.
|
// custom-domain owner could hijack such sessions.
|
||||||
const allowedOrg = getAllowedOrgForSessionID(mreq.sessionID);
|
const allowedOrg = getAllowedOrgForSessionID(mreq.sessionID);
|
||||||
if (allowedOrg) {
|
if (allowedOrg) {
|
||||||
if (allowHost(req, allowedOrg.host)) {
|
if (allowHost(req, allowedOrg.host) || isEnvironmentAllowedHost(allowedOrg.host)) {
|
||||||
customHostSession = ` custom-host-match ${allowedOrg.host}`;
|
customHostSession = ` custom-host-match ${allowedOrg.host}`;
|
||||||
} else {
|
} else {
|
||||||
// We need an exception for internal forwarding from home server to doc-workers. These use
|
// We need an exception for internal forwarding from home server to doc-workers. These use
|
||||||
|
@ -382,8 +382,8 @@ function isFlavor(flavor: string): flavor is keyof typeof spawners {
|
|||||||
* grist-core), and trying to regularize creation options a bit.
|
* grist-core), and trying to regularize creation options a bit.
|
||||||
*
|
*
|
||||||
* The flavor of sandbox to use can be overridden by some environment variables:
|
* The flavor of sandbox to use can be overridden by some environment variables:
|
||||||
* - GRIST_SANDBOX_FLAVOR: should be one of the spawners (pynbox, unsandboxed, docker,
|
* - GRIST_SANDBOX_FLAVOR: should be one of the spawners (gvisor, unsandboxed, docker,
|
||||||
* gvisor, macSandboxExec)
|
* macSandboxExec, pynbox)
|
||||||
* - GRIST_SANDBOX: a program or image name to run as the sandbox. Not needed for
|
* - GRIST_SANDBOX: a program or image name to run as the sandbox. Not needed for
|
||||||
* pynbox (it is either built in or not available). For unsandboxed, should be an
|
* pynbox (it is either built in or not available). For unsandboxed, should be an
|
||||||
* absolute path to python within a virtualenv with all requirements installed.
|
* absolute path to python within a virtualenv with all requirements installed.
|
||||||
@ -391,7 +391,7 @@ function isFlavor(flavor: string): flavor is keyof typeof spawners {
|
|||||||
* in `sandbox/docker`) or a derived image. For gvisor, it should be the full path
|
* in `sandbox/docker`) or a derived image. For gvisor, it should be the full path
|
||||||
* to `sandbox/gvisor/run.py` (if runsc available locally) or to
|
* to `sandbox/gvisor/run.py` (if runsc available locally) or to
|
||||||
* `sandbox/gvisor/wrap_in_docker.sh` (if runsc should be run using the docker
|
* `sandbox/gvisor/wrap_in_docker.sh` (if runsc should be run using the docker
|
||||||
* image built in that directory). Gvisor is not yet available in grist-core.
|
* image built in that directory).
|
||||||
* - PYTHON_VERSION: for gvisor, this is mandatory, and must be set to "2" or "3".
|
* - PYTHON_VERSION: for gvisor, this is mandatory, and must be set to "2" or "3".
|
||||||
* It is ignored by other flavors.
|
* It is ignored by other flavors.
|
||||||
*/
|
*/
|
||||||
|
@ -14,6 +14,7 @@ import {promisifyAll} from 'bluebird';
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import {createClient, Multi, RedisClient} from 'redis';
|
import {createClient, Multi, RedisClient} from 'redis';
|
||||||
|
import {matchesBaseDomain} from 'app/server/lib/requestUtils';
|
||||||
|
|
||||||
promisifyAll(RedisClient.prototype);
|
promisifyAll(RedisClient.prototype);
|
||||||
|
|
||||||
@ -550,6 +551,6 @@ export function isUrlAllowed(urlString: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (process.env.ALLOWED_WEBHOOK_DOMAINS || "").split(",").some(domain =>
|
return (process.env.ALLOWED_WEBHOOK_DOMAINS || "").split(",").some(domain =>
|
||||||
domain && url.host.endsWith(domain)
|
domain && matchesBaseDomain(url.host, domain)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import log from 'app/server/lib/log';
|
|||||||
import {Permit} from 'app/server/lib/Permit';
|
import {Permit} from 'app/server/lib/Permit';
|
||||||
import {Request, Response} from 'express';
|
import {Request, Response} from 'express';
|
||||||
import {URL} from 'url';
|
import {URL} from 'url';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
// log api details outside of dev environment (when GRIST_HOSTED_VERSION is set)
|
// log api details outside of dev environment (when GRIST_HOSTED_VERSION is set)
|
||||||
const shouldLogApiDetails = Boolean(process.env.GRIST_HOSTED_VERSION);
|
const shouldLogApiDetails = Boolean(process.env.GRIST_HOSTED_VERSION);
|
||||||
@ -85,7 +86,7 @@ export function trustOrigin(req: Request, resp: Response): boolean {
|
|||||||
const origin = req.get('origin');
|
const origin = req.get('origin');
|
||||||
if (!origin) { return true; } // Not a CORS request.
|
if (!origin) { return true; } // Not a CORS request.
|
||||||
if (process.env.GRIST_HOST && req.hostname === process.env.GRIST_HOST) { return true; }
|
if (process.env.GRIST_HOST && req.hostname === process.env.GRIST_HOST) { return true; }
|
||||||
if (!allowHost(req, new URL(origin))) { return false; }
|
if (!allowHost(req, new URL(origin)) && !isEnvironmentAllowedHost(new URL(origin))) { return false; }
|
||||||
|
|
||||||
// For a request to a custom domain, the full hostname must match.
|
// For a request to a custom domain, the full hostname must match.
|
||||||
resp.header("Access-Control-Allow-Origin", origin);
|
resp.header("Access-Control-Allow-Origin", origin);
|
||||||
@ -107,10 +108,21 @@ export function allowHost(req: Request, allowedHost: string|URL) {
|
|||||||
// For requests to a native subdomains, only the base domain needs to match.
|
// For requests to a native subdomains, only the base domain needs to match.
|
||||||
const allowedDomain = parseSubdomain(allowedUrl.hostname);
|
const allowedDomain = parseSubdomain(allowedUrl.hostname);
|
||||||
const actualDomain = parseSubdomain(actualUrl.hostname);
|
const actualDomain = parseSubdomain(actualUrl.hostname);
|
||||||
return (actualDomain.base === allowedDomain.base);
|
return (!_.isEmpty(actualDomain) ? actualDomain.base === allowedDomain.base : allowedUrl.hostname === actualUrl.hostname);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.host;
|
||||||
|
return (process.env.GRIST_ALLOWED_HOSTS || "").split(",").some(domain =>
|
||||||
|
domain && matchesBaseDomain(urlHost, domain)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function isParameterOn(parameter: any): boolean {
|
export function isParameterOn(parameter: any): boolean {
|
||||||
return gutil.isAffirmative(parameter);
|
return gutil.isAffirmative(parameter);
|
||||||
}
|
}
|
||||||
|
@ -2791,6 +2791,23 @@ function testDocApi() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Allowed Origin", () => {
|
||||||
|
it('should allow only example.com', async () => {
|
||||||
|
async function checkOrigin(origin: string, status: number, error?: string) {
|
||||||
|
const resp = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/`,
|
||||||
|
{...chimpy, headers: {...chimpy.headers, "Origin": origin}}
|
||||||
|
);
|
||||||
|
error && assert.deepEqual(resp.data, {error});
|
||||||
|
assert.equal(resp.status, status);
|
||||||
|
}
|
||||||
|
await checkOrigin("https://www.toto.com", 500, "Unrecognized origin");
|
||||||
|
await checkOrigin("https://badexample.com", 500, "Unrecognized origin");
|
||||||
|
await checkOrigin("https://bad.com/example.com/toto", 500, "Unrecognized origin");
|
||||||
|
await checkOrigin("https://example.com/path", 200);
|
||||||
|
await checkOrigin("https://good.example.com/toto", 200);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// PLEASE ADD MORE TESTS HERE
|
// PLEASE ADD MORE TESTS HERE
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2884,6 +2901,7 @@ class TestServer {
|
|||||||
REDIS_URL: process.env.TEST_REDIS_URL,
|
REDIS_URL: process.env.TEST_REDIS_URL,
|
||||||
APP_HOME_URL: _homeUrl,
|
APP_HOME_URL: _homeUrl,
|
||||||
ALLOWED_WEBHOOK_DOMAINS: `example.com,localhost:${webhooksTestPort}`,
|
ALLOWED_WEBHOOK_DOMAINS: `example.com,localhost:${webhooksTestPort}`,
|
||||||
|
GRIST_ALLOWED_HOSTS: `example.com,localhost:${webhooksTestPort}`,
|
||||||
...process.env
|
...process.env
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user