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

Fix remote execution vulnerability by switching from execSync to execFileSync #55

Merged
merged 3 commits into from Jun 10, 2020
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
4 changes: 2 additions & 2 deletions src/certificate-authority.ts
Expand Up @@ -43,7 +43,7 @@ export default async function installCertificateAuthority(options: Options = {})
generateKey(rootKeyPath);

debug(`Generating a CA certificate`);
openssl(`req -new -x509 -config "${ caSelfSignConfig }" -key "${ rootKeyPath }" -out "${ rootCACertPath }" -days 825`);
openssl(['req', '-new', '-x509', '-config', caSelfSignConfig, '-key', rootKeyPath, '-out', rootCACertPath, '-days', '825']);

debug('Saving certificate authority credentials');
await saveCertificateAuthorityCredentials(rootKeyPath);
Expand Down Expand Up @@ -82,7 +82,7 @@ async function saveCertificateAuthorityCredentials(keypath: string) {

function certErrors(): string {
try {
openssl(`x509 -in "${ rootCACertPath }" -noout`);
openssl(['x509', '-in', rootCACertPath, '-noout']);
return '';
} catch (e) {
return e.toString();
Expand Down
6 changes: 3 additions & 3 deletions src/certificates.ts
Expand Up @@ -25,22 +25,22 @@ export default async function generateDomainCertificate(domain: string): Promise
debug(`Generating certificate signing request for ${ domain }`);
let csrFile = pathForDomain(domain, `certificate-signing-request.csr`);
withDomainSigningRequestConfig(domain, (configpath) => {
openssl(`req -new -config "${ configpath }" -key "${ domainKeyPath }" -out "${ csrFile }"`);
openssl(['req', '-new', '-config', configpath, '-key', domainKeyPath, '-out', csrFile]);
});

debug(`Generating certificate for ${ domain } from signing request and signing with root CA`);
let domainCertPath = pathForDomain(domain, `certificate.crt`);

await withCertificateAuthorityCredentials(({ caKeyPath, caCertPath }) => {
withDomainCertificateConfig(domain, (domainCertConfigPath) => {
openssl(`ca -config "${ domainCertConfigPath }" -in "${ csrFile }" -out "${ domainCertPath }" -keyfile "${ caKeyPath }" -cert "${ caCertPath }" -days 825 -batch`)
openssl(['ca', '-config', domainCertConfigPath, '-in', csrFile, '-out', domainCertPath, '-keyfile', caKeyPath, '-cert', caCertPath, '-days', '825', '-batch'])
});
});
}

// Generate a cryptographic key, used to sign certificates or certificate signing requests.
export function generateKey(filename: string): void {
debug(`generateKey: ${ filename }`);
openssl(`genrsa -out "${ filename }" 2048`);
openssl(['genrsa', '-out', filename, '2048']);
chmod(filename, 400);
}
2 changes: 2 additions & 0 deletions src/constants.ts
Expand Up @@ -6,6 +6,8 @@ import applicationConfigPath = require('application-config-path');
import eol from 'eol';
import { mktmp } from './utils';

export const VALID_DOMAIN = /(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/;

// Platform shortcuts
export const isMac = process.platform === 'darwin';
export const isLinux = process.platform === 'linux';
Expand Down
6 changes: 5 additions & 1 deletion src/index.ts
Expand Up @@ -9,7 +9,8 @@ import {
pathForDomain,
domainsDir,
rootCAKeyPath,
rootCACertPath
rootCACertPath,
VALID_DOMAIN
} from './constants';
import currentPlatform from './platforms';
import installCertificateAuthority, { ensureCACertReadable, uninstall } from './certificate-authority';
Expand Down Expand Up @@ -65,6 +66,9 @@ type IReturnData<O extends Options = {}> = (IDomainData) & (IReturnCa<O>) & (IRe
* as { caPath: string }
*/
export async function certificateFor<O extends Options>(domain: string, options: O = {} as O): Promise<IReturnData<O>> {
if (!VALID_DOMAIN.test(domain)) {
throw new Error(`"${domain}" is not a valid domain name.`);
}
Comment on lines +69 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zetlen FYI, this looks like it is causing #56. I can open a PR to fix it when I have time, but that might not be for a few days

debug(`Certificate requested for ${ domain }. Skipping certutil install: ${ Boolean(options.skipCertutilInstall) }. Skipping hosts file: ${ Boolean(options.skipHostsFile) }`);

if (options.ui) {
Expand Down
55 changes: 40 additions & 15 deletions src/platforms/darwin.ts
Expand Up @@ -2,14 +2,14 @@ import path from 'path';
import { writeFileSync as writeFile, existsSync as exists, readFileSync as read } from 'fs';
import createDebug from 'debug';
import { sync as commandExists } from 'command-exists';
import { run } from '../utils';
import { run, sudoAppend } from '../utils';
import { Options } from '../index';
import { addCertificateToNSSCertDB, assertNotTouchingFiles, openCertificateInFirefox, closeFirefox, removeCertificateFromNSSCertDB } from './shared';
import { Platform } from '.';

const debug = createDebug('devcert:platforms:macos');

const getCertUtilPath = () => path.join(run('brew --prefix nss').toString().trim(), 'bin', 'certutil');
const getCertUtilPath = () => path.join(run('brew', ['--prefix', 'nss']).toString().trim(), 'bin', 'certutil');

export default class MacOSPlatform implements Platform {

Expand All @@ -30,7 +30,20 @@ export default class MacOSPlatform implements Platform {

// Chrome, Safari, system utils
debug('Adding devcert root CA to macOS system keychain');
run(`sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain -p ssl -p basic "${ certificatePath }"`);
run('sudo', [
'security',
'add-trusted-cert',
'-d',
'-r',
'trustRoot',
'-k',
'/Library/Keychains/System.keychain',
'-p',
'ssl',
'-p',
'basic',
certificatePath
]);

if (this.isFirefoxInstalled()) {
// Try to use certutil to install the cert automatically
Expand All @@ -39,9 +52,13 @@ export default class MacOSPlatform implements Platform {
if (!options.skipCertutilInstall) {
if (commandExists('brew')) {
debug(`certutil is not already installed, but Homebrew is detected. Trying to install certutil via Homebrew...`);
run('brew install nss');
try {
run('brew', ['install', 'nss'], { stdio: 'ignore' });
} catch (e) {
debug(`brew install nss failed`);
}
} else {
debug(`Homebrew isn't installed, so we can't try to install certutil. Falling back to manual certificate install`);
debug(`Homebrew didn't work, so we can't try to install certutil. Falling back to manual certificate install`);
return await openCertificateInFirefox(this.FIREFOX_BIN_PATH, certificatePath);
}
} else {
Expand All @@ -59,7 +76,14 @@ export default class MacOSPlatform implements Platform {
removeFromTrustStores(certificatePath: string) {
debug('Removing devcert root CA from macOS system keychain');
try {
run(`sudo security remove-trusted-cert -d "${ certificatePath }"`);
run('sudo', [
'security',
'remove-trusted-cert',
'-d',
certificatePath
], {
stdio: 'ignore'
});
} catch(e) {
debug(`failed to remove ${ certificatePath } from macOS cert store, continuing. ${ e.toString() }`);
}
Expand All @@ -70,30 +94,31 @@ export default class MacOSPlatform implements Platform {
}

async addDomainToHostFileIfMissing(domain: string) {
const trimDomain = domain.trim().replace(/[\s;]/g,'')
let hostsFileContents = read(this.HOST_FILE_PATH, 'utf8');
if (!hostsFileContents.includes(domain)) {
run(`echo '\n127.0.0.1 ${ domain }' | sudo tee -a "${ this.HOST_FILE_PATH }" > /dev/null`);
if (!hostsFileContents.includes(trimDomain)) {
sudoAppend(this.HOST_FILE_PATH, `127.0.0.1 ${trimDomain}\n`);
}
}

deleteProtectedFiles(filepath: string) {
assertNotTouchingFiles(filepath, 'delete');
run(`sudo rm -rf "${filepath}"`);
run('sudo', ['rm', '-rf', filepath]);
}

async readProtectedFile(filepath: string) {
assertNotTouchingFiles(filepath, 'read');
return (await run(`sudo cat "${filepath}"`)).toString().trim();
return (await run('sudo', ['cat', filepath])).toString().trim();
}

async writeProtectedFile(filepath: string, contents: string) {
assertNotTouchingFiles(filepath, 'write');
if (exists(filepath)) {
await run(`sudo rm "${filepath}"`);
await run('sudo', ['rm', filepath]);
}
writeFile(filepath, contents);
await run(`sudo chown 0 "${filepath}"`);
await run(`sudo chmod 600 "${filepath}"`);
await run('sudo', ['chown', '0', filepath]);
await run('sudo', ['chmod', '600', filepath]);
}

private isFirefoxInstalled() {
Expand All @@ -102,7 +127,7 @@ export default class MacOSPlatform implements Platform {

private isNSSInstalled() {
try {
return run('brew list -1').toString().includes('\nnss\n');
return run('brew', ['list', '-1']).toString().includes('\nnss\n');
} catch (e) {
return false;
}
Expand Down
27 changes: 14 additions & 13 deletions src/platforms/linux.ts
Expand Up @@ -3,7 +3,7 @@ import { existsSync as exists, readFileSync as read, writeFileSync as writeFile
import createDebug from 'debug';
import { sync as commandExists } from 'command-exists';
import { addCertificateToNSSCertDB, assertNotTouchingFiles, openCertificateInFirefox, closeFirefox, removeCertificateFromNSSCertDB } from './shared';
import { run } from '../utils';
import { run, sudoAppend } from '../utils';
import { Options } from '../index';
import UI from '../user-interface';
import { Platform } from '.';
Expand Down Expand Up @@ -32,9 +32,9 @@ export default class LinuxPlatform implements Platform {

debug('Adding devcert root CA to Linux system-wide trust stores');
// run(`sudo cp ${ certificatePath } /etc/ssl/certs/devcert.crt`);
run(`sudo cp "${ certificatePath }" /usr/local/share/ca-certificates/devcert.crt`);
run('sudo', ['cp', certificatePath, '/usr/local/share/ca-certificates/devcert.crt']);
// run(`sudo bash -c "cat ${ certificatePath } >> /etc/ssl/certs/ca-certificates.crt"`);
run(`sudo update-ca-certificates`);
run('sudo', ['update-ca-certificates']);

if (this.isFirefoxInstalled()) {
// Firefox
Expand All @@ -45,7 +45,7 @@ export default class LinuxPlatform implements Platform {
openCertificateInFirefox(this.FIREFOX_BIN_PATH, certificatePath);
} else {
debug('NSS tooling is not already installed. Trying to install NSS tooling now with `apt install`');
run('sudo apt install libnss3-tools');
run('sudo', ['apt', 'install', 'libnss3-tools']);
debug('Installing certificate into Firefox trust stores using NSS tooling');
await closeFirefox();
await addCertificateToNSSCertDB(this.FIREFOX_NSS_DIR, certificatePath, 'certutil');
Expand All @@ -70,8 +70,8 @@ export default class LinuxPlatform implements Platform {

removeFromTrustStores(certificatePath: string) {
try {
run(`sudo rm /usr/local/share/ca-certificates/devcert.crt`);
run(`sudo update-ca-certificates`);
run('sudo', ['rm', '/usr/local/share/ca-certificates/devcert.crt']);
run('sudo', ['update-ca-certificates']);
} catch (e) {
debug(`failed to remove ${ certificatePath } from /usr/local/share/ca-certificates, continuing. ${ e.toString() }`);
}
Expand All @@ -86,30 +86,31 @@ export default class LinuxPlatform implements Platform {
}

async addDomainToHostFileIfMissing(domain: string) {
const trimDomain = domain.trim().replace(/[\s;]/g,'')
let hostsFileContents = read(this.HOST_FILE_PATH, 'utf8');
if (!hostsFileContents.includes(domain)) {
run(`echo '127.0.0.1 ${ domain }' | sudo tee -a "${ this.HOST_FILE_PATH }" > /dev/null`);
if (!hostsFileContents.includes(trimDomain)) {
sudoAppend(this.HOST_FILE_PATH, `127.0.0.1 ${trimDomain}\n`);
}
}

deleteProtectedFiles(filepath: string) {
assertNotTouchingFiles(filepath, 'delete');
run(`sudo rm -rf "${filepath}"`);
run('sudo', ['rm', '-rf', filepath]);
}

async readProtectedFile(filepath: string) {
assertNotTouchingFiles(filepath, 'read');
return (await run(`sudo cat "${filepath}"`)).toString().trim();
return (await run('sudo', ['cat', filepath])).toString().trim();
}

async writeProtectedFile(filepath: string, contents: string) {
assertNotTouchingFiles(filepath, 'write');
if (exists(filepath)) {
await run(`sudo rm "${filepath}"`);
await run('sudo', ['rm', filepath]);
}
writeFile(filepath, contents);
await run(`sudo chown 0 "${filepath}"`);
await run(`sudo chmod 600 "${filepath}"`);
await run('sudo', ['chown', '0', filepath]);
await run('sudo', ['chmod', '600', filepath]);
}

private isFirefoxInstalled() {
Expand Down
6 changes: 3 additions & 3 deletions src/platforms/shared.ts
Expand Up @@ -39,7 +39,7 @@ export function addCertificateToNSSCertDB(nssDirGlob: string, certPath: string,
debug(`trying to install certificate into NSS databases in ${ nssDirGlob }`);
doForNSSCertDB(nssDirGlob, (dir, version) => {
const dirArg = version === 'modern' ? `sql:${ dir }` : dir;
run(`${ certutilPath } -A -d "${ dirArg }" -t 'C,,' -i "${ certPath }" -n devcert`)
run(certutilPath, ['-A', '-d', dirArg, '-t', 'C,,', '-i', certPath, '-n', 'devcert']);
});
debug(`finished scanning & installing certificate in NSS databases in ${ nssDirGlob }`);
}
Expand All @@ -49,7 +49,7 @@ export function removeCertificateFromNSSCertDB(nssDirGlob: string, certPath: str
doForNSSCertDB(nssDirGlob, (dir, version) => {
const dirArg = version === 'modern' ? `sql:${ dir }` : dir;
try {
run(`${ certutilPath } -A -d "${ dirArg }" -t 'C,,' -i "${ certPath }" -n devcert`)
run(certutilPath, ['-A', '-d', dirArg, '-t', 'C,,', '-i', certPath, '-n', 'devcert']);
} catch (e) {
debug(`failed to remove ${ certPath } from ${ dir }, continuing. ${ e.toString() }`)
}
Expand Down Expand Up @@ -124,7 +124,7 @@ export async function openCertificateInFirefox(firefoxPath: string, certPath: st
}).listen(port);
debug('Certificate server is up. Printing instructions for user and launching Firefox with hosted certificate URL');
await UI.startFirefoxWizard(`http://localhost:${ port }`);
run(`${ firefoxPath } http://localhost:${ port }`);
run(firefoxPath, [`http://localhost:${ port }`]);
await UI.waitForFirefoxWizard();
server.close();
}
Expand Down
4 changes: 2 additions & 2 deletions src/platforms/win32.ts
Expand Up @@ -28,7 +28,7 @@ export default class WindowsPlatform implements Platform {
// IE, Chrome, system utils
debug('adding devcert root to Windows OS trust store')
try {
run(`certutil -addstore -user root "${ certificatePath }"`);
run('certutil', ['-addstore', '-user', 'root', certificatePath]);
} catch (e) {
e.output.map((buffer: Buffer) => {
if (buffer) {
Expand All @@ -49,7 +49,7 @@ export default class WindowsPlatform implements Platform {
debug('removing devcert root from Windows OS trust store');
try {
console.warn('Removing old certificates from trust stores. You may be prompted to grant permission for this. It\'s safe to delete old devcert certificates.');
run(`certutil -delstore -user root devcert`);
run('certutil', ['-delstore', '-user', 'root', 'devcert']);
} catch (e) {
debug(`failed to remove ${ certificatePath } from Windows OS trust store, continuing. ${ e.toString() }`)
}
Expand Down
18 changes: 12 additions & 6 deletions src/utils.ts
@@ -1,4 +1,4 @@
import { execSync, ExecSyncOptions } from 'child_process';
import { execFileSync, ExecFileSyncOptions } from 'child_process';
import tmp from 'tmp';
import createDebug from 'debug';
import path from 'path';
Expand All @@ -8,18 +8,24 @@ import { configPath } from './constants';

const debug = createDebug('devcert:util');

export function openssl(cmd: string) {
return run(`openssl ${ cmd }`, {
export function openssl(args: string[]) {
return run('openssl', args, {
stdio: 'pipe',
env: Object.assign({
RANDFILE: path.join(configPath('.rnd'))
}, process.env)
});
}

export function run(cmd: string, options: ExecSyncOptions = {}) {
debug(`exec: \`${ cmd }\``);
return execSync(cmd, options);
export function run(cmd: string, args: string[], options: ExecFileSyncOptions = {}) {
debug(`execFileSync: \`${ cmd } ${args.join(' ')}\``);
return execFileSync(cmd, args, options);
}

export function sudoAppend(file: string, input: ExecFileSyncOptions["input"]) {
run('sudo', ['tee', '-a', file], {
input
});
}

export function waitForUser() {
Expand Down