From bb858fe71ffa20a6be256fa27c1bfa9e44a503e5 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 15 Oct 2024 19:43:28 +0200 Subject: [PATCH] feat: default enable allowDynamicBackends with better unsupported errors (#995) --- .../docs/backend/Backend/Backend.mdx | 67 +------- .../docs/backend/Backend/prototype/name.mdx | 76 ++++++++++ .../docs/backend/Backend/prototype/toName.mdx | 6 + .../backend/Backend/prototype/toString.mdx | 6 + .../docs/backend/allowDynamicBackends.mdx | 38 +++++ .../docs/backend/enforceExplicitBackends.mdx | 43 ++++++ documentation/docs/globals/fetch.mdx | 20 +++ .../fixtures/app/src/dynamic-backend.js | 19 ++- runtime/fastly/builtins/backend.cpp | 56 ++++++- runtime/fastly/builtins/backend.h | 4 +- runtime/fastly/builtins/fastly.cpp | 7 +- .../builtins/fetch/request-response.cpp | 2 +- runtime/fastly/host-api/error_numbers.msg | 2 + src/bundle.js | 6 + tests/wpt-harness/post-harness.js | 3 +- types/backend.d.ts | 143 +++++++++--------- types/experimental.d.ts | 27 ++-- types/geolocation.d.ts | 4 +- types/globals.d.ts | 4 +- types/kv-store.d.ts | 2 - 20 files changed, 368 insertions(+), 167 deletions(-) create mode 100644 documentation/docs/backend/Backend/prototype/name.mdx create mode 100644 documentation/docs/backend/allowDynamicBackends.mdx create mode 100644 documentation/docs/backend/enforceExplicitBackends.mdx diff --git a/documentation/docs/backend/Backend/Backend.mdx b/documentation/docs/backend/Backend/Backend.mdx index 79a4f6d59a..3db45735ce 100644 --- a/documentation/docs/backend/Backend/Backend.mdx +++ b/documentation/docs/backend/Backend/Backend.mdx @@ -10,18 +10,9 @@ import {Fiddle} from '@site/src/components/fiddle'; The **`Backend` constructor** lets you dynamically create new [Fastly Backends](https://developer.fastly.com/reference/api/services/backend/) for your Fastly Compute service. -Dynamically creating new [Fastly Backends](https://developer.fastly.com/reference/api/services/backend/) is disabled by default for Fastly Services. Please contact [Fastly Support](https://support.fastly.com/hc/requests/new?ticket_form_id=360000269711) to request the feature be enabled on the Fastly Services which require Dynamic Backends. +>**Note**: Dynamic backends are by default disabled at the Fastly service level. Contact [Fastly Support](https://support.fastly.com/hc/en-us/requests/new?ticket_form_id=360000269711) to request dynamic backends on Fastly Services. -By default, Dynamic Backends are disabled within a JavaScript application as it can be a potential avenue for third-party JavaScript code to send requests, potentially including sensitive/secret data, off to destinations that the JavaScript project was not intending, which could be a security issue. - -To enable Dynamic Backends the application will need to explicitly allow Dynamic Backends via: - -```js -import { allowDynamicBackends } from "fastly:experimental"; -allowDynamicBackends(true); -``` - -**Note**: Backend constructors can only be used when processing requests, not during build-time initialization. +To disable the usage of dynamic backends, see [enforceExplicitBackends](../enforceExplicitBackends.mdx). ## Syntax @@ -122,58 +113,8 @@ A new `Backend` object. ## Examples -In this example an implicit Dynamic Backend is created when making the fetch request to and the response is then returned to the client. - -import { allowDynamicBackends } from "fastly:experimental"; -allowDynamicBackends(true); -async function app() { - // For any request, return the fastly homepage -- without defining a backend! - return fetch('https://www.fastly.com/'); -} -addEventListener("fetch", event => event.respondWith(app(event))); -` - }, - "requests": [ - { - "enableCluster": true, - "enableShield": false, - "enableWAF": false, - "method": "GET", - "path": "/status=200", - "useFreshCache": false, - "followRedirects": false, - "tests": "", - "delay": 0 - } - ], - "srcVersion": 1 -}}> - -```js -/// -import { allowDynamicBackends } from "fastly:experimental"; -allowDynamicBackends(true); -async function app() { - // For any request, return the fastly homepage -- without defining a backend! - return fetch('https://www.fastly.com/'); -} -addEventListener("fetch", event => event.respondWith(app(event))); -``` - - - In this example an explicit Dynamic Backend is created and supplied to the fetch request, the response is then returned to the client. - -import { allowDynamicBackends } from "fastly:experimental"; import { Backend } from "fastly:backend"; -allowDynamicBackends(true); async function app() { // For any request, return the fastly homepage -- without defining a backend! const backend = new Backend({ @@ -225,9 +164,7 @@ addEventListener("fetch", event => event.respondWith(app(event))); ```js /// -import { allowDynamicBackends } from "fastly:experimental"; import { Backend } from "fastly:backend"; -allowDynamicBackends(true); async function app() { // For any request, return the fastly homepage -- without defining a backend! const backend = new Backend({ diff --git a/documentation/docs/backend/Backend/prototype/name.mdx b/documentation/docs/backend/Backend/prototype/name.mdx new file mode 100644 index 0000000000..bb4081114b --- /dev/null +++ b/documentation/docs/backend/Backend/prototype/name.mdx @@ -0,0 +1,76 @@ +--- +hide_title: false +hide_table_of_contents: false +pagination_next: null +pagination_prev: null +--- +import {Fiddle} from '@site/src/components/fiddle'; + +# name + +The read-only **`name`** property of the backend returns the backend name string. + +## Value + +A `string`. + +## Description + +Provides the name of the backend. + +## Examples + +### Using name + +The following example logs the string value of a [Backend](../Backend.mdx) object: + + +import { Backend } from "fastly:backend"; +async function app() { + const backend = new Backend({ + name: "fastly", + target: "fastly.com", + }); + console.log(backend.name); // "fastly" +} +addEventListener("fetch", event => event.respondWith(app(event))); +` + }, + "requests": [ + { + "enableCluster": true, + "enableShield": false, + "enableWAF": false, + "method": "GET", + "path": "/status=200", + "useFreshCache": false, + "followRedirects": false, + "tests": "", + "delay": 0 + } + ], + "srcVersion": 1 +}}> + +```js +import { Backend } from "fastly:backend"; +async function app() { + const backend = new Backend({ + name: "fastly", + target: "fastly.com", + }); + console.log(backend.name); // "fastly" +} +addEventListener("fetch", event => event.respondWith(app(event))); +``` + + \ No newline at end of file diff --git a/documentation/docs/backend/Backend/prototype/toName.mdx b/documentation/docs/backend/Backend/prototype/toName.mdx index f7667882de..751cbef129 100644 --- a/documentation/docs/backend/Backend/prototype/toName.mdx +++ b/documentation/docs/backend/Backend/prototype/toName.mdx @@ -7,6 +7,12 @@ pagination_prev: null # Backend.prototype.toName() +:::info + +This method is deprecated, use [`Backend.prototype.name`](./name.mdx) instead. + +::: + The **`toName()`** method returns the name associated with the `Backend` instance. ## Syntax diff --git a/documentation/docs/backend/Backend/prototype/toString.mdx b/documentation/docs/backend/Backend/prototype/toString.mdx index 8dd7a21ec9..5e8bb1a987 100644 --- a/documentation/docs/backend/Backend/prototype/toString.mdx +++ b/documentation/docs/backend/Backend/prototype/toString.mdx @@ -8,6 +8,12 @@ import {Fiddle} from '@site/src/components/fiddle'; # toString +:::info + +This method is deprecated, use [`Backend.prototype.name`](./name.mdx) instead. + +::: + The **`toString()`** method returns a string representing the specified Backend value. ## Syntax diff --git a/documentation/docs/backend/allowDynamicBackends.mdx b/documentation/docs/backend/allowDynamicBackends.mdx new file mode 100644 index 0000000000..ede53cc65f --- /dev/null +++ b/documentation/docs/backend/allowDynamicBackends.mdx @@ -0,0 +1,38 @@ +--- +hide_title: false +hide_table_of_contents: false +pagination_next: null +pagination_prev: null +--- +import {Fiddle} from '@site/src/components/fiddle'; + +# allowDynamicBackends + +:::info + +This method is deprecated, and dynamic backends are now always supported when enabled at the service level. See [`enforceExplicitBackends`](./enforceExplicitBackends.mdx) instead. + +::: + +The **`allowDynamicBackends()`** function is used to control whether or not Dynamic Backends should be allowed within this Fastly Compute Service. + +By default, Dynamic Backends are enabled, but can be a potential security concern since third-party JavaScript code may send arbitrary requests, potentially including sensitive/secret data, off to destinations that the JavaScript project was not intending. + +Using `allowDynamicBackends(false)` this security property can be restored to only use explicit backend definitions. + +>**Note**: By default, while dynamic backends are allowed in the SDK, they are by default disabled at the Fastly service level. + +## Syntax + +```js +allowDynamicBackends(enabledOrConfig) +``` + +### Parameters + +- `enabled` _: boolean_ + - Whether or not to allow Dynamic Backends + +### Return value + +`undefined`. diff --git a/documentation/docs/backend/enforceExplicitBackends.mdx b/documentation/docs/backend/enforceExplicitBackends.mdx new file mode 100644 index 0000000000..9c4bf95ced --- /dev/null +++ b/documentation/docs/backend/enforceExplicitBackends.mdx @@ -0,0 +1,43 @@ +--- +hide_title: false +hide_table_of_contents: false +pagination_next: null +pagination_prev: null +--- + +# enforceExplicitBackends + +Call this function to enforce the security property of explicitly-defined backends, even when dynamic backends are enabled at +the Fastly service level. + +By default, if dynamic backends are supported for the Fastly service, they will be automatically used when creating a new +`fetch()` request. This default behaviour for dynamic backends can be a potential security concern since third-party JavaScript +code may send arbitrary requests, including sensitive/secret data, off to destinations that the JavaScript project was not +intending. + +When calling this function, an optional default backend name can be provided. + +>**Note**: This is a separate option to the service-level dynamic backend support for Fastly services, which is by deault disabled for Fastly services. + +The **`enforceExplicitBackends()`** function is used to control whether or not Dynamic Backends should be allowed within this Fastly Compute Service. + +By default, Dynamic Backends are enabled, but can be a potential security concern since third-party JavaScript code may send arbitrary requests, potentially including sensitive/secret data, off to destinations that the JavaScript project was not intending. + +Using `allowDynamicBackends(false)` this security property can be restored to only use explicit backend definitions. + +>**Note**: Dynamic Backends are disabled by default for Fastly Services. Please contact [Fastly Support](https://support.fastly.com/hc/requests/new?ticket_form_id=360000269711) to request the feature be enabled or disabled on Fastly Services. + +## Syntax + +```js +enforceExplicitBackends(defaultBackend?) +``` + +### Parameters + +- `defaultBackend` _: string_ _**optional**_ + - An optional default backend string name to use in `fetch()` requests. + +### Return value + +`undefined`. diff --git a/documentation/docs/globals/fetch.mdx b/documentation/docs/globals/fetch.mdx index 32581670e4..a8f05b89b3 100644 --- a/documentation/docs/globals/fetch.mdx +++ b/documentation/docs/globals/fetch.mdx @@ -23,6 +23,26 @@ not_ reject on HTTP errors (`404`, etc.). Instead, a > **Note:** The `fetch()` method's parameters are identical to > those of the `Request()` constructor. +## Explicit Backends + +Internally, Fastly uses named backends to handle fetch requests, which need to be explicitly defined to enable custom HTTP origins to be fetched by the service. + +This `backend` option is then a special Fastly-specific fetch option that is provided to the `fetch()` call: + +```js +fetch('https://origin.com/path', { backend: 'origin' }); +``` + +Backends are configured using the Fastly service backend configuration, see the [Backend documentation](https://developer.fastly.com/reference/api/services/backend/) for more information. + +## Dynamic Backends + +Dynamic backends are a compute feature that allow services to define backends for themselves. This is a service-level Fastly feature that must be enabled through [Fastly Support](https://support.fastly.com/hc/en-us/requests/new?ticket_form_id=360000269711). + +When dynamic backends are enabled at the service level, the explicit `backend` option is no longer required for `fetch()` requests, and will instead be automatically created. + +In addition, custom backend confiuration options can then also be provided through the [`Backend()`](../fastly:backend/Backend/Backend.mdx) constructor. + ## Syntax ```js diff --git a/integration-tests/js-compute/fixtures/app/src/dynamic-backend.js b/integration-tests/js-compute/fixtures/app/src/dynamic-backend.js index f47505f45e..a3c3d12a81 100644 --- a/integration-tests/js-compute/fixtures/app/src/dynamic-backend.js +++ b/integration-tests/js-compute/fixtures/app/src/dynamic-backend.js @@ -1,7 +1,11 @@ /// -import { Backend, setDefaultDynamicBackendConfig } from 'fastly:backend'; -import { CacheOverride } from 'fastly:cache-override'; +import { + Backend, + setDefaultDynamicBackendConfig, + enforceExplicitBackends, +} from 'fastly:backend'; import { allowDynamicBackends } from 'fastly:experimental'; +import { CacheOverride } from 'fastly:cache-override'; import { assert, assertDoesNotThrow, @@ -55,6 +59,10 @@ routes.set('/backend/timeout', async () => { allowDynamicBackends(true); await assertResolves(() => fetch('https://http-me.glitch.me/headers')); await assertResolves(() => fetch('https://www.fastly.com')); + enforceExplicitBackends(); + await assertRejects(() => fetch('https://www.fastly.com')); + enforceExplicitBackends('TheOrigin'); + await assertResolves(() => fetch('https://www.fastly.com')); }); routes.set( '/implicit-dynamic-backend/dynamic-backends-enabled-called-twice', @@ -225,6 +233,7 @@ routes.set('/backend/timeout', async () => { actual = Reflect.ownKeys(Backend.prototype); expected = [ 'constructor', + 'name', 'isDynamic', 'target', 'hostOverride', @@ -2432,6 +2441,9 @@ routes.set('/backend/timeout', async () => { { const backend = createValidFastlyBackend() ?? validFastlyBackend; strictEqual(backend.isDynamic, true, 'isDymamic'); + strictEqual(backend.name, 'fastly'); + strictEqual(backend.toString(), 'fastly'); + strictEqual(backend.toName(), 'fastly'); strictEqual(backend.target, 'www.fastly.com', 'target'); strictEqual(backend.hostOverride, 'www.fastly.com', 'override'); strictEqual(backend.port, 443, 'port'); @@ -2466,6 +2478,9 @@ routes.set('/backend/timeout', async () => { { const backend = createValidHttpMeBackend() ?? validHttpMeBackend; strictEqual(backend.isDynamic, true, 'isDynamic'); + strictEqual(backend.name, 'http-me'); + strictEqual(backend.toString(), 'http-me'); + strictEqual(backend.toName(), 'http-me'); strictEqual(backend.target, 'http-me.glitch.me', 'target'); strictEqual(backend.hostOverride, 'http-me.glitch.me', 'hostOverride'); strictEqual(backend.port, 443, 'port'); diff --git a/runtime/fastly/builtins/backend.cpp b/runtime/fastly/builtins/backend.cpp index 1bdea1f50a..eca9e35ba8 100644 --- a/runtime/fastly/builtins/backend.cpp +++ b/runtime/fastly/builtins/backend.cpp @@ -1259,12 +1259,12 @@ JSString *Backend::name(JSContext *cx, JSObject *self) { return JS_NewStringCopyZ(cx, backend->name().begin()); } -bool Backend::to_string(JSContext *cx, unsigned argc, JS::Value *vp) { +bool Backend::name_get(JSContext *cx, unsigned argc, JS::Value *vp) { METHOD_HEADER(0); auto backend = get_backend(cx, self); if (!backend) { - args.rval().setMagic(JSWhyMagic::JS_UNINITIALIZED_LEXICAL); + args.rval().setUndefined(); return true; } auto &name = backend->name(); @@ -1658,10 +1658,11 @@ const JSFunctionSpec Backend::static_methods[] = { JS_FN("exists", exists, 1, JSPROP_ENUMERATE), JS_FN("fromName", from_name, 1, JSPROP_ENUMERATE), JS_FN("health", health_for_name, 1, JSPROP_ENUMERATE), JS_FS_END}; const JSPropertySpec Backend::static_properties[] = {JS_PS_END}; -const JSFunctionSpec Backend::methods[] = {JS_FN("toString", to_string, 0, JSPROP_ENUMERATE), - JS_FN("toName", to_string, 0, JSPROP_ENUMERATE), +const JSFunctionSpec Backend::methods[] = {JS_FN("toString", name_get, 0, JSPROP_ENUMERATE), + JS_FN("toName", name_get, 0, JSPROP_ENUMERATE), JS_FS_END}; const JSPropertySpec Backend::properties[] = { + JS_PSG("name", name_get, JSPROP_ENUMERATE), JS_PSG("isDynamic", is_dynamic_get, JSPROP_ENUMERATE), JS_PSG("target", target_get, JSPROP_ENUMERATE), JS_PSG("hostOverride", host_override_get, JSPROP_ENUMERATE), @@ -1751,6 +1752,12 @@ JSObject *Backend::create(JSContext *cx, JS::HandleObject request) { auto res = host_api::HttpReq::register_dynamic_backend(host_backend->name(), target_string, backend_config); if (auto *err = res.to_err()) { + if (host_api::error_is_unsupported(*err)) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_DYNAMIC_BACKENDS_UNSUPPORTED_IMPLICIT, target_string.data(), + url_string.data); + return nullptr; + } HANDLE_ERROR(cx, *err); return nullptr; } @@ -1814,6 +1821,12 @@ bool Backend::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { auto res = host_api::HttpReq::register_dynamic_backend(host_backend->name(), target_string, backend_config); if (auto *err = res.to_err()) { + if (host_api::error_is_unsupported(*err)) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_DYNAMIC_BACKENDS_UNSUPPORTED_EXPLICIT, + target_string_slice.data); + return false; + } HANDLE_ERROR(cx, *err); return false; } @@ -1846,6 +1859,29 @@ bool set_default_backend_config(JSContext *cx, unsigned argc, JS::Value *vp) { return true; } +// TODO: in next major, when global and fastly experimental are deprecated, +// make it so that calling twice always throws an already enforced error. +// and possibly also don't allow changing the default again. +bool enforce_explicit_backends(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + auto default_backend_val = args.get(0); + if (!default_backend_val.isNullOrUndefined()) { + if (!default_backend_val.isString()) { + api::throw_error(cx, api::Errors::TypeError, "enforceExplicitBackends", "defaultBackend", + "be undefined or a string"); + return false; + } + JS::RootedString backend(cx, JS::ToString(cx, default_backend_val)); + if (!backend) { + return false; + } + Fastly::defaultBackend = backend; + } + Fastly::allowDynamicBackends = false; + args.rval().setUndefined(); + return true; +} + bool install(api::Engine *engine) { JS::RootedObject backends(engine->cx(), JS_NewPlainObject(engine->cx())); if (!backends) { @@ -1873,6 +1909,18 @@ bool install(api::Engine *engine) { set_default_backend_config_val)) { return false; } + + auto enforce_explicit_backends_fn = + JS_NewFunction(engine->cx(), &enforce_explicit_backends, 1, 0, "enforceExplicitBackends"); + RootedObject enforce_explicit_backends_obj(engine->cx(), + JS_GetFunctionObject(enforce_explicit_backends_fn)); + RootedValue enforce_explicit_backends_val(engine->cx(), + JS::ObjectValue(*enforce_explicit_backends_obj)); + if (!JS_SetProperty(engine->cx(), backend_ns, "enforceExplicitBackends", + enforce_explicit_backends_val)) { + return false; + } + RootedValue backend_ns_val(engine->cx(), JS::ObjectValue(*backend_ns)); if (!engine->define_builtin_module("fastly:backend", backend_ns_val)) { return false; diff --git a/runtime/fastly/builtins/backend.h b/runtime/fastly/builtins/backend.h index d2c7f1fc10..9dead35d43 100644 --- a/runtime/fastly/builtins/backend.h +++ b/runtime/fastly/builtins/backend.h @@ -31,10 +31,10 @@ class Backend : public builtins::FinalizableBuiltinImpl { static bool health_for_name(JSContext *cx, unsigned argc, JS::Value *vp); // prototype methods - static bool to_name(JSContext *cx, unsigned argc, JS::Value *vp); - static bool to_string(JSContext *cx, unsigned argc, JS::Value *vp); static bool health(JSContext *cx, unsigned argc, JS::Value *vp); + // getters + static bool name_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool is_dynamic_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool target_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool host_override_get(JSContext *cx, unsigned argc, JS::Value *vp); diff --git a/runtime/fastly/builtins/fastly.cpp b/runtime/fastly/builtins/fastly.cpp index 80effbb2e0..73222fce20 100644 --- a/runtime/fastly/builtins/fastly.cpp +++ b/runtime/fastly/builtins/fastly.cpp @@ -49,7 +49,8 @@ const JSErrorFormatString *FastlyGetErrorMessage(void *userRef, unsigned errorNu JS::PersistentRooted Fastly::env; JS::PersistentRooted Fastly::baseURL; JS::PersistentRooted Fastly::defaultBackend; -bool Fastly::allowDynamicBackends = false; +bool allowDynamicBackendsCalled = false; +bool Fastly::allowDynamicBackends = true; bool Fastly::dump(JSContext *cx, unsigned argc, JS::Value *vp) { JS::CallArgs args = CallArgsFromVp(argc, vp); @@ -373,6 +374,9 @@ bool Fastly::defaultBackend_set(JSContext *cx, unsigned argc, JS::Value *vp) { return false; defaultBackend = backend; + if (!allowDynamicBackendsCalled) { + allowDynamicBackends = false; + } args.rval().setUndefined(); return true; } @@ -395,6 +399,7 @@ bool Fastly::allowDynamicBackends_set(JSContext *cx, unsigned argc, JS::Value *v } else { allowDynamicBackends = JS::ToBoolean(set_value); } + allowDynamicBackendsCalled = true; args.rval().setUndefined(); return true; } diff --git a/runtime/fastly/builtins/fetch/request-response.cpp b/runtime/fastly/builtins/fetch/request-response.cpp index c04e183c49..86e437536f 100644 --- a/runtime/fastly/builtins/fetch/request-response.cpp +++ b/runtime/fastly/builtins/fetch/request-response.cpp @@ -1796,7 +1796,7 @@ JSObject *Request::create(JSContext *cx, JS::HandleObject requestInstance, JS::H if (!url_instance) return nullptr; - JS::RootedObject parsedURL(cx, URL::create(cx, url_instance, input, WorkerLocation::url)); + JS::RootedObject parsedURL(cx, URL::create(cx, url_instance, input, fastly::Fastly::baseURL)); // 2. If `parsedURL` is failure, then throw a `TypeError`. if (!parsedURL) { diff --git a/runtime/fastly/host-api/error_numbers.msg b/runtime/fastly/host-api/error_numbers.msg index 534091d223..34c0684ac6 100644 --- a/runtime/fastly/host-api/error_numbers.msg +++ b/runtime/fastly/host-api/error_numbers.msg @@ -79,6 +79,8 @@ MSG_DEF(JSMSG_SECRET_STORE_NAME_TOO_LONG, 0, JSEXN_TYPEERR, MSG_DEF(JSMSG_SECRET_STORE_FROM_BYTES_INVALID_BUFFER, 0, JSEXN_TYPEERR, "SecretStore.fromBytes: bytes must be an ArrayBuffer or ArrayBufferView object") MSG_DEF(JSMSG_READABLE_STREAM_LOCKED_OR_DISTRUBED, 0, JSEXN_TYPEERR, "Can't use a ReadableStream that's locked or has ever been read from or canceled") MSG_DEF(JSMSG_INVALID_CHARACTER_ERROR, 0, JSEXN_ERR, "String contains an invalid character") +MSG_DEF(JSMSG_DYNAMIC_BACKENDS_UNSUPPORTED_EXPLICIT, 2, JSEXN_ERR, "fetch(): No backend provided fetching '{1}'. Since dynamic backends are not enabled for this Fastly service, `fetch()` requires an explicit backend parameter. See https://js-compute-reference-docs.edgecompute.app/docs/globals/fetch for more info. Alternatively, contact Fastly support to enable dynamic backends.") +MSG_DEF(JSMSG_DYNAMIC_BACKENDS_UNSUPPORTED_IMPLICIT, 1, JSEXN_ERR, "Backend constructor: Unable to create a dynamic backend for '{0}' - dynamic backends are unsupported on this service. Either explicitly configure backend services or contact Fastly support to enable dynamic backends.") MSG_DEF(JSMSG_BACKEND_FROMNAME_BACKEND_DOES_NOT_EXIST, 1, JSEXN_ERR, "Backend.fromName: backend named '{0}' does not exist") MSG_DEF(JSMSG_BACKEND_IS_HEALTHY_BACKEND_DOES_NOT_EXIST, 1, JSEXN_ERR, "Backend.health: backend named '{0}' does not exist") MSG_DEF(JSMSG_BACKEND_PARAMETER_NOT_OBJECT, 0, JSEXN_TYPEERR, "Backend constructor: configuration parameter must be an Object") diff --git a/src/bundle.js b/src/bundle.js index 342e2ae28e..1bd6213b1e 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -16,6 +16,12 @@ let fastlyPlugin = { contents: ` export const Backend = globalThis.Backend; export const setDefaultDynamicBackendConfig = Object.getOwnPropertyDescriptor(globalThis.fastly, 'allowDynamicBackends').set; +const allowDynamicBackends = Object.getOwnPropertyDescriptor(globalThis.fastly, 'allowDynamicBackends').set; +export const setDefaultBackend = Object.getOwnPropertyDescriptor(globalThis.fastly, 'defaultBackend').set; +export function enforceExplicitBackends (defaultBackend) { + allowDynamicBackends(false); + if (defaultBackend) setDefaultBackend(defaultBackend); +} `, }; } diff --git a/tests/wpt-harness/post-harness.js b/tests/wpt-harness/post-harness.js index 3599701e6f..0f105b2ca8 100644 --- a/tests/wpt-harness/post-harness.js +++ b/tests/wpt-harness/post-harness.js @@ -1,9 +1,8 @@ /* eslint-env serviceworker */ /* global add_completion_callback setup done */ -import { enableDebugLogging, setDefaultBackend, setBaseURL } from "fastly:experimental"; +import { enableDebugLogging, setBaseURL } from "fastly:experimental"; enableDebugLogging(true); -setDefaultBackend("wpt"); let completionPromise = new Promise((resolve) => { add_completion_callback(function(tests, harness_status, asserts) { diff --git a/types/backend.d.ts b/types/backend.d.ts index 25a8d1d87b..acb66963ce 100644 --- a/types/backend.d.ts +++ b/types/backend.d.ts @@ -1,10 +1,47 @@ /// +/// declare module 'fastly:backend' { + /** + * Set the default backend configuration options for dynamic backends. + * + * Applies to backends created via {@link Backend | new Backend(...)} as well as for dynamic backends + * implicitly created when using {@link fetch | fetch()}. + * + * @note + * Dynamic backends are by default disabled at the Fastly service level. + * Contact [Fastly Support](https://support.fastly.com/hc/en-us/requests/new?ticket_form_id=360000269711) + * to request dynamic backends on Fastly Services. + * + * @param defaultDynamicBackendConfiguration default backend configuration options + */ export function setDefaultDynamicBackendConfig( defaultDynamicBackendConfiguration: DefaultBackendConfiguration, ): void; + /** + * Call this function to enforce the security property of explicitly-defined backends, even + * when dynamic backends are enabled at the Fastly service level. + * + * By default, if dynamic backends are supported for the Fastly service, they will be automatically + * used when creating a new `fetch()` request. This default behaviour for dynamic backends can be a + * potential security concern since third-party JavaScript code may send arbitrary requests, + * including sensitive/secret data, off to destinations that the JavaScript project was not + * intending. + * + * When calling this function, an optional default backend name can be provided. + * + * @note + * This is a separate option to the service-level dynamic backend support for Fastly services. + * By default, dynamic backends are disabled for Fastly Services, so that even if not using this + * function, the service-level security configuration will apply. + * + * @param defaultBackend the name of the default backend to use, when using {@link fetch | fetch()}. + * + * @experimental + */ + export function enforceExplicitBackends(defaultBackend?: string): void; + interface DefaultBackendConfiguration { /** * Maximum duration in milliseconds to wait for a connection to this backend to be established. @@ -87,23 +124,25 @@ declare module 'fastly:backend' { * Enables and sets the TCP keep alive options for the backend. * Setting to boolean true enables keepalive with the default options. */ - tcpKeepalive?: boolean | { - /** - * Configure how long to wait after the last sent data over the TCP connection before - * starting to send TCP keepalive probes. - */ - timeSecs?: number; + tcpKeepalive?: + | boolean + | { + /** + * Configure how long to wait after the last sent data over the TCP connection before + * starting to send TCP keepalive probes. + */ + timeSecs?: number; - /** - * Configure how long to wait between each TCP keepalive probe sent to the backend to determine if it is still active. - */ - intervalSecs?: number; + /** + * Configure how long to wait between each TCP keepalive probe sent to the backend to determine if it is still active. + */ + intervalSecs?: number; - /** - * Number of probes to send to the backend before it is considered dead. - */ - probes?: number; - }; + /** + * Number of probes to send to the backend before it is considered dead. + */ + probes?: number; + }; } interface BackendConfiguration extends DefaultBackendConfiguration { @@ -168,63 +207,11 @@ declare module 'fastly:backend' { * Class for dynamically creating new [Fastly Backends](https://developer.fastly.com/reference/api/services/backend/). * * @note - * This feature is in disabled by default for Fastly Services. Please contact [Fastly Support](https://support.fastly.com/hc/en-us/requests/new?ticket_form_id=360000269711) to request the feature be enabled on the Fastly Services which require Dynamic Backends. - * - * By default, Dynamic Backends are disabled within a JavaScript application as it can be a potential - * avenue for third-party JavaScript code to send requests, potentially including sensitive/secret data, - * off to destinations that the JavaScript project was not intending, which could be a security issue. - * - * To enable Dynamic Backends the application will need to explicitly allow Dynamic Backends via: - * ```js - * import { allowDynamicBackends } from "fastly:experimental"; - * allowDynamicBackends(true); - * ``` + * Dynamic backends are by default disabled at the Fastly service level. + * Contact [Fastly Support](https://support.fastly.com/hc/en-us/requests/new?ticket_form_id=360000269711) + * to request dynamic backends on Fastly Services. * - * **Note**: Can only be used when processing requests, not during build-time initialization. - * - * @example - * - * In this example an implicit Dynamic Backend is created when making the fetch request to https://www.fastly.com/ and the response is then returned to the client. - * - * - * + * To disable the usage of dynamic backends, see {@link enforceExplicitBackends}. * * @example * In this example an explicit Dynamic Backend is created and supplied to the fetch request, the response is then returned to the client. @@ -238,7 +225,7 @@ declare module 'fastly:backend' { * ], * "src": { * "deps": "{\n \"@fastly/js-compute\": \"^0.7.0\"\n}", - * "main": "/// \nimport { allowDynamicBackends } from \"fastly:experimental\";\nimport { Backend } from \"fastly:backend\";\nallowDynamicBackends(true);\nasync function app() {\n // For any request, return the fastly homepage -- without defining a backend!\n const backend = new Backend({\n name: 'fastly',\n target: 'fastly.com',\n hostOverride: \"www.fastly.com\",\n connectTimeout: 1000,\n firstByteTimeout: 15000,\n betweenBytesTimeout: 10000,\n useSSL: true,\n sslMinVersion: 1.3,\n sslMaxVersion: 1.3,\n });\n return fetch('https://www.fastly.com/', {\n backend // Here we are configuring this request to use the backend from above.\n });\n}\naddEventListener(\"fetch\", event => event.respondWith(app(event)));\n" + * "main": "/// \nimport { Backend } from \"fastly:backend\";\nasync function app() {\n // For any request, return the fastly homepage -- without defining a backend!\n const backend = new Backend({\n name: 'fastly',\n target: 'fastly.com',\n hostOverride: \"www.fastly.com\",\n connectTimeout: 1000,\n firstByteTimeout: 15000,\n betweenBytesTimeout: 10000,\n useSSL: true,\n sslMinVersion: 1.3,\n sslMaxVersion: 1.3,\n });\n return fetch('https://www.fastly.com/', {\n backend // Here we are configuring this request to use the backend from above.\n });\n}\naddEventListener(\"fetch\", event => event.respondWith(app(event)));\n" * }, * "requests": [ * { @@ -259,9 +246,7 @@ declare module 'fastly:backend' { *