Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Increase security by virtualizing $env/static/* #5825

Merged
merged 18 commits into from Aug 16, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lovely-boxes-give.md
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[feat] `$env/static/*` are now virtual to prevent writing sensitive values to disk
2 changes: 2 additions & 0 deletions packages/kit/src/core/constants.js
@@ -1,3 +1,5 @@
// in `vite dev` and `vite preview`, we use a fake asset path so that we can
// serve local assets while verifying that requests are correctly prefixed
export const SVELTE_KIT_ASSETS = '/_svelte_kit_assets';

export const GENERATED_COMMENT = '// this file is generated — do not edit it\n';
91 changes: 91 additions & 0 deletions packages/kit/src/core/env.js
@@ -0,0 +1,91 @@
import { GENERATED_COMMENT } from './constants.js';

/**
* @param {string} id
* @param {Record<string, string>} env
* @returns {string}
*/
export function create_module(id, env) {
/** @type {string[]} */
const declarations = [];

for (const key in env) {
if (!valid_identifier.test(key) || reserved.has(key)) {
continue;
}

const comment = `/** @type {import('${id}').${key}} */`;
const declaration = `export const ${key} = ${JSON.stringify(env[key])};`;

declarations.push(`${comment}\n${declaration}`);
}

return GENERATED_COMMENT + declarations.join('\n\n');
}

/**
* @param {string} id
* @param {Record<string, string>} env
* @returns {string}
*/
export function create_types(id, env) {
const declarations = Object.keys(env)
.filter((k) => valid_identifier.test(k))
.map((k) => `\texport const ${k}: string;`)
.join('\n');

return `declare module '${id}' {\n${declarations}\n}`;
}

export const reserved = new Set([
'do',
'if',
'in',
'for',
'let',
'new',
'try',
'var',
'case',
'else',
'enum',
'eval',
'null',
'this',
'true',
'void',
'with',
'await',
'break',
'catch',
'class',
'const',
'false',
'super',
'throw',
'while',
'yield',
'delete',
'export',
'import',
'public',
'return',
'static',
'switch',
'typeof',
'default',
'extends',
'finally',
'package',
'private',
'continue',
'debugger',
'function',
'arguments',
'interface',
'protected',
'implements',
'instanceof'
]);

export const valid_identifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
53 changes: 0 additions & 53 deletions packages/kit/src/core/sync/utils.js
Expand Up @@ -42,56 +42,3 @@ export function trim(str) {
const pattern = new RegExp(`^${indentation}`, 'gm');
return str.replace(pattern, '').trim();
}

export const reserved = new Set([
'do',
'if',
'in',
'for',
'let',
'new',
'try',
'var',
'case',
'else',
'enum',
'eval',
'null',
'this',
'true',
'void',
'with',
'await',
'break',
'catch',
'class',
'const',
'false',
'super',
'throw',
'while',
'yield',
'delete',
'export',
'import',
'public',
'return',
'static',
'switch',
'typeof',
'default',
'extends',
'finally',
'package',
'private',
'continue',
'debugger',
'function',
'arguments',
'interface',
'protected',
'implements',
'instanceof'
]);

export const valid_identifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
72 changes: 6 additions & 66 deletions packages/kit/src/core/sync/write_ambient.js
@@ -1,9 +1,9 @@
import path from 'path';
import colors from 'kleur';
import { get_env } from '../../vite/utils.js';
import { write_if_changed, reserved, valid_identifier } from './utils.js';
import { GENERATED_COMMENT } from '../constants.js';
import { create_types } from '../env.js';
import { write_if_changed } from './utils.js';

const autogen_comment = '// this file is generated — do not edit it\n';
const types_reference = '/// <reference types="@sveltejs/kit" />\n\n';

/**
Expand All @@ -16,72 +16,12 @@ const types_reference = '/// <reference types="@sveltejs/kit" />\n\n';
export function write_ambient(config, mode) {
const env = get_env(mode, config.env.publicPrefix);

// TODO when testing src, `$app` points at `src/runtime/app`... will
// probably need to fiddle with aliases
write_if_changed(
path.join(config.outDir, 'runtime/env/static/public.js'),
create_env_module('$env/static/public', env.public)
);

write_if_changed(
path.join(config.outDir, 'runtime/env/static/private.js'),
create_env_module('$env/static/private', env.private)
);

write_if_changed(
path.join(config.outDir, 'ambient.d.ts'),
autogen_comment +
GENERATED_COMMENT +
types_reference +
create_env_types('$env/static/public', env.public) +
create_types('$env/static/public', env.public) +
'\n\n' +
create_env_types('$env/static/private', env.private)
create_types('$env/static/private', env.private)
);
}

/**
* @param {string} id
* @param {Record<string, string>} env
* @returns {string}
*/
function create_env_module(id, env) {
/** @type {string[]} */
const declarations = [];

for (const key in env) {
const warning = !valid_identifier.test(key)
? 'not a valid identifier'
: reserved.has(key)
? 'a reserved word'
: null;

if (warning) {
console.error(
colors
.bold()
.yellow(`Omitting environment variable "${key}" from ${id} as it is ${warning}`)
);
continue;
}

const comment = `/** @type {import('${id}').${key}} */`;
const declaration = `export const ${key} = ${JSON.stringify(env[key])};`;

declarations.push(`${comment}\n${declaration}`);
}

return autogen_comment + declarations.join('\n\n');
}

/**
* @param {string} id
* @param {Record<string, string>} env
* @returns {string}
*/
function create_env_types(id, env) {
const declarations = Object.keys(env)
.filter((k) => valid_identifier.test(k))
.map((k) => `\texport const ${k}: string;`)
.join('\n');

return `declare module '${id}' {\n${declarations}\n}`;
}
35 changes: 27 additions & 8 deletions packages/kit/src/vite/index.js
Expand Up @@ -14,8 +14,9 @@ import { generate_manifest } from '../core/generate_manifest/index.js';
import { runtime_directory, logger } from '../core/utils.js';
import { find_deps, get_default_config as get_default_build_config } from './build/utils.js';
import { preview } from './preview/index.js';
import { get_aliases, resolve_entry, prevent_illegal_rollup_imports } from './utils.js';
import { get_aliases, resolve_entry, prevent_illegal_rollup_imports, get_env } from './utils.js';
import { fileURLToPath } from 'node:url';
import { create_module } from '../core/env.js';

const cwd = process.cwd();

Expand Down Expand Up @@ -107,6 +108,9 @@ function kit() {
/** @type {string | undefined} */
let deferred_warning;

/** @type {{ public: Record<string, string>; private: Record<string, string> }} */
let env;

/**
* @type {{
* build_dir: string;
Expand Down Expand Up @@ -191,13 +195,13 @@ function kit() {
* @see https://vitejs.dev/guide/api-plugin.html#config
*/
async config(config, config_env) {
// The config is created in build_server for SSR mode and passed inline
if (config.build?.ssr) {
return;
}

vite_config_env = config_env;
svelte_config = await load_config();
env = get_env(vite_config_env.mode, svelte_config.kit.env.publicPrefix);

// The config is created in build_server for SSR mode and passed inline
if (config.build?.ssr) return;

is_build = config_env.command === 'build';

paths = {
Expand Down Expand Up @@ -269,6 +273,20 @@ function kit() {
return result;
},

async resolveId(id) {
// treat $env/static/[public|private] as virtual
if (id.startsWith('$env/static/')) return `\0${id}`;
},

async load(id) {
switch (id) {
case '\0$env/static/private':
return create_module('$env/static/private', env.private);
case '\0$env/static/public':
return create_module('$env/static/public', env.public);
}
},

/**
* Stores the final config.
*/
Expand Down Expand Up @@ -432,9 +450,10 @@ function kit() {
await adapt(svelte_config, build_data, prerendered, { log });
} else {
console.log(colors.bold().yellow('\nNo adapter specified'));
// prettier-ignore

const link = colors.bold().cyan('https://kit.svelte.dev/docs/adapters');
console.log(
`See ${colors.bold().cyan('https://kit.svelte.dev/docs/adapters')} to learn how to configure your app to run on the platform of your choosing`
`See ${link} to learn how to configure your app to run on the platform of your choosing`
);
}

Expand Down
17 changes: 2 additions & 15 deletions packages/kit/src/vite/utils.js
Expand Up @@ -105,6 +105,8 @@ export function get_aliases(config) {
const alias = [
{ find: '__GENERATED__', replacement: path.posix.join(config.outDir, 'generated') },
{ find: '$app', replacement: `${runtime_directory}/app` },
{ find: '$env/dynamic/public', replacement: `${runtime_directory}/env/dynamic/public.js` },
{ find: '$env/dynamic/private', replacement: `${runtime_directory}/env/dynamic/private.js` },
// For now, we handle `$lib` specially here rather than make it a default value for
// `config.kit.alias` since it has special meaning for packaging, etc.
{ find: '$lib', replacement: config.files.lib }
Expand All @@ -128,21 +130,6 @@ export function get_aliases(config) {
}
}

alias.push(
{
find: '$env/static/public',
replacement: path.posix.join(config.outDir, 'runtime/env/static/public.js')
},
{
find: '$env/static/private',
replacement: path.posix.join(config.outDir, 'runtime/env/static/private.js')
},
{
find: '$env',
replacement: `${runtime_directory}/env`
}
);

return alias;
}

Expand Down