Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions docs/pages/advanced/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ index: 4

`Browser.newPage()` supports a `sandbox` mode, which use
[Deno permissions](https://docs.deno.com/runtime/manual/basics/permissions) to
validate network requests (using `--allow-net` permissions) and file requests
(using `--allow-read` permissions) on the opened page.
validate network requests (using `--allow-net` permissions), file requests
(using `--allow-read` permissions) and imported `<script>` requests (using
`--allow-import` permissions) on the opened page.

## Code

Expand Down Expand Up @@ -51,14 +52,15 @@ You can choose to pass a
to use a subset of these permissions instead and further restrict what a given
page can access.

Currently both
[`Deno.ReadPermissionDescriptor`](https://docs.deno.com/api/deno/~/Deno.ReadPermissionDescriptor)
Currently
[`Deno.ReadPermissionDescriptor`](https://docs.deno.com/api/deno/~/Deno.ReadPermissionDescriptor),
[`Deno.NetPermissionDescriptor`](https://docs.deno.com/api/deno/~/Deno.NetPermissionDescriptor),
and
[`Deno.NetPermissionDescriptor`](https://docs.deno.com/api/deno/~/Deno.NetPermissionDescriptor)
[`Deno.ImportPermissionDescriptor`](https://docs.deno.com/api/deno/~/Deno.ImportPermissionDescriptor)
are supported.

Using `true` (e.g. `net: true` / `read: true`) is the same as using `"inherit"`
and will not throw any permission escalation error.
Using `true` (e.g. `net: true` / `read: true` / `import: true`) is the same as
using `"inherit"` and will not throw any permission escalation error.

```ts
await using browser = await launch();
Expand Down
1 change: 1 addition & 0 deletions src/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ if (!querySync) {
env: "granted",
sys: "denied",
ffi: "denied",
import: "denied",
} as const;

querySync = ({ name }) => {
Expand Down
11 changes: 8 additions & 3 deletions src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export type SandboxOptions = {
permissions:
| "inherit"
| "none"
| Pick<Deno.PermissionOptionsObject, "read" | "net">;
| Pick<Deno.PermissionOptionsObject, "read" | "net" | "import">;
};
};

Expand Down Expand Up @@ -288,6 +288,7 @@ export class Page extends EventTarget implements AsyncDisposable {
if (
!await this.#validateRequest(
e.detail,
resourceType,
options as SandboxNormalizedOptions,
)
) {
Expand Down Expand Up @@ -315,12 +316,13 @@ export class Page extends EventTarget implements AsyncDisposable {

async #validateRequest(
{ request }: Fetch_requestPausedEvent["detail"],
resourceType: Network_ResourceType,
sandbox: SandboxNormalizedOptions,
) {
const { protocol, host, href } = new URL(request.url);
if (host) {
return (await this.#getPermissionState(sandbox, {
name: "net",
name: resourceType === "Script" ? "import" : "net",
host,
})) === "granted";
}
Expand All @@ -336,7 +338,10 @@ export class Page extends EventTarget implements AsyncDisposable {

async #getPermissionState(
{ sandbox: { permissions } }: SandboxNormalizedOptions,
descriptor: Deno.NetPermissionDescriptor | Deno.ReadPermissionDescriptor,
descriptor:
| Deno.NetPermissionDescriptor
| Deno.ReadPermissionDescriptor
| Deno.ImportPermissionDescriptor,
) {
if (permissions === "none") {
return "denied";
Expand Down
76 changes: 73 additions & 3 deletions tests/sandbox_test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { getDefaultCachePath, launch } from "../mod.ts";
import { assertStrictEquals } from "@std/assert";
import { assertStrictEquals, assertStringIncludes } from "@std/assert";
import { fromFileUrl } from "@std/path/from-file-url";
import { assert } from "@std/assert/assert";

const cache = getDefaultCachePath();

const permissions = { read: [cache], write: true, run: true, env: true };
const permissions = {
read: [cache],
write: true,
run: true,
env: true,
import: true,
};
const status =
"window.performance.getEntriesByType('navigation')[0].responseStatus";

Expand Down Expand Up @@ -70,10 +76,29 @@ Deno.test("Sandbox supports granular permissions", {
...permissions,
read: [...permissions.read, fromFileUrl(import.meta.url)],
net: true,
import: ["127.0.0.1"],
},
}, async (t) => {
await using server = Deno.serve((request) => {
switch (new URL(request.url).pathname) {
case "/redirect.js":
return new Response(
`location = "http://127.0.0.1:${server.addr.port}/ok"`,
{ status: 200, headers: { "content-type": "text/javascript" } },
);
case "/ok":
return new Response("<!DOCTYPE html><body>IMPORT_SUCCESS</body>", {
status: 202,
headers: { "content-type": "text/html" },
});
default:
return new Response("Not Found", { status: 404 });
}
}) as Deno.HttpServer<Deno.NetAddr>;
const importPermissionTestUrl =
`data:text/html,<!DOCTYPE html><body>IMPORT_PENDING<script src="http://127.0.0.1:${server.addr.port}/redirect.js"></script></body>`;
for (
const { url, code, sandbox } of [
const { url, code, sandbox, includes } of [
{
url: "http://example.com",
code: 200,
Expand Down Expand Up @@ -150,6 +175,48 @@ Deno.test("Sandbox supports granular permissions", {
sandbox: { permissions: { read: false } },
},
{ url: import.meta.url, code: 0, sandbox: { permissions: { read: [] } } },
{
url: importPermissionTestUrl,
code: 202,
sandbox: { permissions: "inherit" as const },
includes: "IMPORT_SUCCESS",
},
{
url: importPermissionTestUrl,
code: 202,
sandbox: { permissions: { import: true } },
includes: "IMPORT_SUCCESS",
},
{
url: importPermissionTestUrl,
code: 202,
sandbox: { permissions: { import: undefined } },
includes: "IMPORT_SUCCESS", // inherit from parent context
},
{
url: importPermissionTestUrl,
code: 202,
sandbox: { permissions: { import: ["127.0.0.1"] } },
includes: "IMPORT_SUCCESS",
},
{
url: importPermissionTestUrl,
code: 200,
sandbox: { permissions: "none" as const },
includes: "IMPORT_PENDING",
},
{
url: importPermissionTestUrl,
code: 200,
sandbox: { permissions: { import: false } },
includes: "IMPORT_PENDING",
},
{
url: importPermissionTestUrl,
code: 200,
sandbox: { permissions: { import: [] } },
includes: "IMPORT_PENDING",
},
]
) {
await t.step(
Expand All @@ -160,6 +227,9 @@ Deno.test("Sandbox supports granular permissions", {
await using browser = await launch();
await using page = await browser.newPage(url, { sandbox });
assertStrictEquals(await page.evaluate(status), code);
if (includes) {
assertStringIncludes(await page.content(), includes);
}
},
);
}
Expand Down
Loading