Skip to content
Open
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
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ The existence of this plugin does not mean vite is pushing users security out in

A plugin to enhance security of the dev server in [Vite](https://vitejs.dev).




## Installation

```bash
Expand Down
2 changes: 1 addition & 1 deletion packages/playground/vite.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from 'vite';
import { protect } from '@svitejs/vite-plugin-protect';
export default defineConfig({
plugins: [protect({ auth: { applyToPreview: true } })],
plugins: [protect({ auth: false, pair: { applyToPreview: true } })],
server: {
fs: {
deny: ['.env', '.env.*', '*.{crt,pem}', '**/.git/**']
Expand Down
31 changes: 26 additions & 5 deletions packages/vite-plugin-protect/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createAuthMiddleWare } from './auth-middleware.js';
import { createAuthMiddleWare } from './middleware/auth.js';
import { createPairMiddleware } from './middleware/pair.js';
import { hookFs } from './hook-fs.js';

/**
Expand All @@ -11,7 +12,11 @@ function applyDefaults(opts) {
fs: true,
auth: {
realm: 'vite-dev-server',
username: 'vite'
username: 'vite',
applyToPreview: true
},
pair: {
applyToPreview: true
},
onViolation: {
log: true,
Expand All @@ -22,15 +27,25 @@ function applyDefaults(opts) {
return {
fs: opts?.fs ?? defaultOptions.fs,
auth:
opts?.auth === false
opts?.auth === false || opts?.auth === undefined
? false
: opts?.auth === true || opts?.auth === undefined
: opts?.auth === true
? defaultOptions.auth
: {
// @ts-ignore
...defaultOptions.auth,
...opts.auth
},
pair:
opts?.pair === false
? false
: opts?.pair === true || opts?.pair === undefined
? defaultOptions.pair
: {
// @ts-ignore
...defaultOptions.pair,
...opts.pair
},
onViolation: {
...defaultOptions.onViolation,
...opts?.onViolation
Expand Down Expand Up @@ -67,12 +82,18 @@ export function protect(opts) {
if (options.auth) {
s.middlewares.use(createAuthMiddleWare(options));
}
if (options.pair) {
s.middlewares.use(createPairMiddleware(options));
}
},
configurePreviewServer(s) {
options.server = s;
if (typeof options.auth === 'object' && options.auth.applyToPreview) {
if (options.auth?.applyToPreview) {
s.middlewares.use(createAuthMiddleWare(options));
}
if (options.pair?.applyToPreview) {
s.middlewares.use(createPairMiddleware(options));
}
}
};
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import auth from 'basic-auth';
import { onViolation } from './on-violation.js';

function getRemote(req) {
return req.client?.remoteAddress?.replace('::ffff:', '');
}
function isLocal(req) {
const r = getRemote(req);
return r && ['127.0.0.1', 'localhost', '::1'].includes(r);
}
import { onViolation } from '../on-violation.js';
import { getRemote, isLocal } from './utils.js';

export function createAuthMiddleWare(options) {
const password =
Expand Down
88 changes: 88 additions & 0 deletions packages/vite-plugin-protect/src/middleware/pair.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { onViolation } from '../on-violation.js';
import { getRemote, isLocal } from './utils.js';

function initPairing() {
const codeLen = 6;
const code = Math.floor(Math.random() * Math.pow(10, codeLen))
.toString()
.padStart(codeLen, '0');
return {
code,
status: 'pending'
};
}

const pairHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {display:grid;place-items:center;height:100dvh}
form {display:flex;flex-direction: column;gap: 0.5rem;width: 7rem}
input[type="number"] {appearance: textfield;-webkit-appearance: textfield;font-family: monospace}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {-webkit-appearance:none;margin:0}
button,input {font-size:1.5rem;}
label {font-size:1.25rem;text-align:center}
</style>
</head>
<body>
<form action="." method="POST" enctype="text/plain" accept-charset="utf-8">
<label for="__pair_code">Enter code</label>
<input type="number" name="__pair_code" id="__pair_code" autofocus maxlength="6" min="0" max="999999" pattern="[0-9]{6}">
<button type="submit">submit</button>
</form>
</body>
</html>
`;

export function createPairMiddleware(options) {
const pairings = new Map();
return function pairMiddleware(req, res, next) {
if (options?.pair?.applyToLocalRequests || !isLocal(req)) {
const ip = getRemote(req);
let pairing = pairings.get(ip);
if (!pairing || (pairing.status === 'pending' && pairing.expires < Date.now())) {
pairing = initPairing();
pairings.set(ip, pairing);
console.log(
`[PROTECT PAIR] ip ${ip} is trying to connect to this dev server. Enter code ${pairing.code} on that device`
);
}
if (pairing.status === 'successful') {
next();
} else if (pairing.status === 'failed') {
res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0');
res.statusCode = 403;
res.end('forbidden');
} else if (pairing.status === 'pending') {
if (req.method === 'GET') {
res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0');
res.statusCode = 200;
res.end(pairHtml);
} else if (req.method === 'POST') {
let body = '';
req.on('data', (/** @type {string} */ c) => (body += c));
req.on('end', () => {
const code = body.match(/^__pair_code=(\d+)/)?.[1];
if (code != null) {
pairing.status = pairing.code === code ? 'successful' : 'failed';
if (pairing.status === 'failed') {
onViolation({
msg: 'Pairing failed',
details: `remoteAddress: ${getRemote(req)}`,
options
});
}
}
//reload after post so pairing status gets reevaluated
res.setHeader('Location', req.url);
res.statusCode = 302;
res.end();
});
}
}
} else {
next();
}
};
}
8 changes: 8 additions & 0 deletions packages/vite-plugin-protect/src/middleware/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function getRemote(req) {
return req.client?.remoteAddress?.replace('::ffff:', '');
}

export function isLocal(req) {
const r = getRemote(req);
return r && ['127.0.0.1', 'localhost', '::1'].includes(r);
}
38 changes: 34 additions & 4 deletions packages/vite-plugin-protect/src/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ export interface PluginOptions {
* require authentication for requests to the dev server
*
* - false: no authentication
* - true: default configuration: authenticate remote requests with username "vite" and generated password
* - true: automatic configuration: authenticate remote requests with username "vite" and generated password
* - object: custom configuration
*
* @default true
* @default false
*/
auth?:
| boolean
Expand All @@ -29,7 +29,7 @@ export interface PluginOptions {

/**
* also authenticate local requests.
* These can theoretically be abused to escalate priviliges from another local process
* These can theoretically be abused to escalate privileges from another local process
* that cannot access the files that the dev server has access to
*
* @default false
Expand All @@ -38,6 +38,36 @@ export interface PluginOptions {

/**
* also authenticate server started with vite preview
*
* @default true
*/
applyToPreview?: boolean;
};
/**
* require entering a pairing code to allow requests to the dev server
*
* - false: no pairing
* - true: automatic configuration: require pairing for remote requests
* - object: custom configuration
*
* @default true
*/
pair?:
| boolean
| {
/**
* pair local requests.
* These can theoretically be abused to escalate privileges from another local process
* that cannot access the files that the dev server has access to
*
* @default false
*/
applyToLocalRequests?: boolean;

/**
* pair for preview server
*
* @default true
*/
applyToPreview?: boolean;
};
Expand Down Expand Up @@ -66,7 +96,7 @@ export interface PluginOptions {
showOverlay?: boolean;

/**
* shutdown devserver on violation to eliminate further risk
* shutdown dev server on violation to eliminate further risk
* @default false
*/
shutdown?: boolean;
Expand Down
Loading