@bitwarden/mcp-server 2025.7.1 → 2025.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.js +130 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,6 +147,7 @@ The server provides the following Bitwarden CLI tools:
|
|
|
147
147
|
- **Use environment variables** for sensitive configuration
|
|
148
148
|
- **Validate all inputs** using Zod schemas (already implemented)
|
|
149
149
|
- **Test with non-production data** when possible
|
|
150
|
+
- Understand the security and privacy impacts of exposing sensitive vault data to LLM and AI tools. Using a self-hosted or local LLM may be appropriate, for example.
|
|
150
151
|
|
|
151
152
|
## Troubleshooting
|
|
152
153
|
|
package/dist/index.js
CHANGED
|
@@ -492,7 +492,101 @@ export function validateInput(schema, args) {
|
|
|
492
492
|
}
|
|
493
493
|
const execPromise = promisify(exec);
|
|
494
494
|
/**
|
|
495
|
-
*
|
|
495
|
+
* Sanitizes a string to prevent command injection by removing dangerous characters
|
|
496
|
+
* and escape sequences that could be used to execute arbitrary commands.
|
|
497
|
+
*
|
|
498
|
+
* @param {string} input - The input string to sanitize
|
|
499
|
+
* @returns {string} The sanitized string safe for use in shell commands
|
|
500
|
+
*/
|
|
501
|
+
export function sanitizeInput(input) {
|
|
502
|
+
if (typeof input !== 'string') {
|
|
503
|
+
throw new Error('Input must be a string');
|
|
504
|
+
}
|
|
505
|
+
// Remove or escape dangerous characters that could be used for command injection
|
|
506
|
+
return (input
|
|
507
|
+
// Remove null bytes
|
|
508
|
+
.replace(/\0/g, '')
|
|
509
|
+
// Remove command separators and operators
|
|
510
|
+
.replace(/[;&|`$(){}[\]<>'"]/g, '')
|
|
511
|
+
// Remove escape sequences and control characters
|
|
512
|
+
.replace(/\\./g, '')
|
|
513
|
+
// Remove newlines and carriage returns
|
|
514
|
+
.replace(/[\r\n]/g, '')
|
|
515
|
+
// Remove tab characters
|
|
516
|
+
.replace(/\t/g, ' ')
|
|
517
|
+
// Collapse multiple spaces
|
|
518
|
+
.replace(/\s+/g, ' ')
|
|
519
|
+
// Trim whitespace
|
|
520
|
+
.trim());
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Safely escapes a parameter value for use in shell commands by using single quotes
|
|
524
|
+
* and properly escaping any single quotes within the value.
|
|
525
|
+
*
|
|
526
|
+
* @param {string} value - The parameter value to escape
|
|
527
|
+
* @returns {string} The safely escaped parameter
|
|
528
|
+
*/
|
|
529
|
+
export function escapeShellParameter(value) {
|
|
530
|
+
if (typeof value !== 'string') {
|
|
531
|
+
throw new Error('Parameter must be a string');
|
|
532
|
+
}
|
|
533
|
+
// Replace single quotes with '\'' (end quote, escaped quote, start quote)
|
|
534
|
+
// This is the safest way to handle single quotes in shell parameters
|
|
535
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Builds a safe Bitwarden CLI command with properly escaped parameters.
|
|
539
|
+
*
|
|
540
|
+
* @param {string} baseCommand - The base command (e.g., 'get', 'list')
|
|
541
|
+
* @param {string[]} parameters - Array of parameters to append safely
|
|
542
|
+
* @returns {string} The safely constructed command
|
|
543
|
+
*/
|
|
544
|
+
export function buildSafeCommand(baseCommand, parameters = []) {
|
|
545
|
+
// Sanitize the base command
|
|
546
|
+
const sanitizedBase = sanitizeInput(baseCommand);
|
|
547
|
+
// Escape all parameters
|
|
548
|
+
const escapedParams = parameters.map((param) => escapeShellParameter(param));
|
|
549
|
+
// Combine base command with escaped parameters
|
|
550
|
+
return [sanitizedBase, ...escapedParams].join(' ');
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Validates that a command is safe and contains only allowed Bitwarden CLI commands.
|
|
554
|
+
*
|
|
555
|
+
* @param {string} command - The command to validate
|
|
556
|
+
* @returns {boolean} True if the command is safe, false otherwise
|
|
557
|
+
*/
|
|
558
|
+
export function isValidBitwardenCommand(command) {
|
|
559
|
+
// List of allowed Bitwarden CLI commands
|
|
560
|
+
const allowedCommands = [
|
|
561
|
+
'lock',
|
|
562
|
+
'unlock',
|
|
563
|
+
'sync',
|
|
564
|
+
'status',
|
|
565
|
+
'list',
|
|
566
|
+
'get',
|
|
567
|
+
'generate',
|
|
568
|
+
'create',
|
|
569
|
+
'edit',
|
|
570
|
+
'delete',
|
|
571
|
+
'confirm',
|
|
572
|
+
'import',
|
|
573
|
+
'export',
|
|
574
|
+
'serve',
|
|
575
|
+
'config',
|
|
576
|
+
'login',
|
|
577
|
+
'logout',
|
|
578
|
+
];
|
|
579
|
+
// Split command into parts
|
|
580
|
+
const parts = command.trim().split(/\s+/);
|
|
581
|
+
if (parts.length === 0) {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
// First part should be a valid Bitwarden command
|
|
585
|
+
const baseCommand = parts[0];
|
|
586
|
+
return allowedCommands.includes(baseCommand);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Executes a Bitwarden CLI command safely with input sanitization and validation.
|
|
496
590
|
*
|
|
497
591
|
* @async
|
|
498
592
|
* @param {string} command - The Bitwarden CLI command to execute (without 'bw' prefix)
|
|
@@ -500,7 +594,16 @@ const execPromise = promisify(exec);
|
|
|
500
594
|
*/
|
|
501
595
|
async function executeCliCommand(command) {
|
|
502
596
|
try {
|
|
503
|
-
|
|
597
|
+
// Sanitize the command input
|
|
598
|
+
const sanitizedCommand = sanitizeInput(command);
|
|
599
|
+
// Validate that it's a safe Bitwarden command
|
|
600
|
+
if (!isValidBitwardenCommand(sanitizedCommand)) {
|
|
601
|
+
return {
|
|
602
|
+
errorOutput: 'Invalid or unsafe command. Only Bitwarden CLI commands are allowed.',
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
// Execute the sanitized command with bw prefix
|
|
606
|
+
const { stdout, stderr } = await execPromise(`bw ${sanitizedCommand}`);
|
|
504
607
|
return {
|
|
505
608
|
output: stdout,
|
|
506
609
|
errorOutput: stderr,
|
|
@@ -534,7 +637,7 @@ async function runServer() {
|
|
|
534
637
|
console.error('Bitwarden MCP Server starting ...');
|
|
535
638
|
const server = new Server({
|
|
536
639
|
name: 'Bitwarden MCP Server',
|
|
537
|
-
version: '2025.7.
|
|
640
|
+
version: '2025.7.2',
|
|
538
641
|
}, {
|
|
539
642
|
capabilities: {
|
|
540
643
|
tools: {},
|
|
@@ -648,7 +751,8 @@ async function runServer() {
|
|
|
648
751
|
return validationResult;
|
|
649
752
|
}
|
|
650
753
|
const { object, id } = validationResult;
|
|
651
|
-
const
|
|
754
|
+
const command = buildSafeCommand('get', [object, id]);
|
|
755
|
+
const result = await executeCliCommand(command);
|
|
652
756
|
return {
|
|
653
757
|
content: [
|
|
654
758
|
{
|
|
@@ -722,7 +826,10 @@ async function runServer() {
|
|
|
722
826
|
};
|
|
723
827
|
const folderJson = JSON.stringify(folderObject);
|
|
724
828
|
const folderBase64 = Buffer.from(folderJson, 'utf8').toString('base64');
|
|
725
|
-
const createCommand =
|
|
829
|
+
const createCommand = buildSafeCommand('create', [
|
|
830
|
+
'folder',
|
|
831
|
+
folderBase64,
|
|
832
|
+
]);
|
|
726
833
|
const result = await executeCliCommand(createCommand);
|
|
727
834
|
return {
|
|
728
835
|
content: [
|
|
@@ -761,7 +868,10 @@ async function runServer() {
|
|
|
761
868
|
}
|
|
762
869
|
const itemJson = JSON.stringify(itemObject);
|
|
763
870
|
const itemBase64 = Buffer.from(itemJson, 'utf8').toString('base64');
|
|
764
|
-
const createCommand =
|
|
871
|
+
const createCommand = buildSafeCommand('create', [
|
|
872
|
+
'item',
|
|
873
|
+
itemBase64,
|
|
874
|
+
]);
|
|
765
875
|
const result = await executeCliCommand(createCommand);
|
|
766
876
|
return {
|
|
767
877
|
content: [
|
|
@@ -783,7 +893,8 @@ async function runServer() {
|
|
|
783
893
|
const { objectType, id, name: itemName, notes, login, } = validationResult;
|
|
784
894
|
if (objectType === 'folder') {
|
|
785
895
|
// Edit folder
|
|
786
|
-
const
|
|
896
|
+
const command = buildSafeCommand('get', ['folder', id]);
|
|
897
|
+
const getResult = await executeCliCommand(command);
|
|
787
898
|
if (getResult.errorOutput) {
|
|
788
899
|
return {
|
|
789
900
|
content: [
|
|
@@ -817,7 +928,11 @@ async function runServer() {
|
|
|
817
928
|
}
|
|
818
929
|
const folderJson = JSON.stringify(currentFolder);
|
|
819
930
|
const folderBase64 = Buffer.from(folderJson, 'utf8').toString('base64');
|
|
820
|
-
const editCommand =
|
|
931
|
+
const editCommand = buildSafeCommand('edit', [
|
|
932
|
+
'folder',
|
|
933
|
+
id,
|
|
934
|
+
folderBase64,
|
|
935
|
+
]);
|
|
821
936
|
const result = await executeCliCommand(editCommand);
|
|
822
937
|
return {
|
|
823
938
|
content: [
|
|
@@ -833,7 +948,8 @@ async function runServer() {
|
|
|
833
948
|
}
|
|
834
949
|
else {
|
|
835
950
|
// Edit item
|
|
836
|
-
const
|
|
951
|
+
const command = buildSafeCommand('get', ['item', id]);
|
|
952
|
+
const getResult = await executeCliCommand(command);
|
|
837
953
|
if (getResult.errorOutput) {
|
|
838
954
|
return {
|
|
839
955
|
content: [
|
|
@@ -883,7 +999,11 @@ async function runServer() {
|
|
|
883
999
|
// Perform the edit
|
|
884
1000
|
const itemJson = JSON.stringify(currentItem);
|
|
885
1001
|
const itemBase64 = Buffer.from(itemJson, 'utf8').toString('base64');
|
|
886
|
-
const editCommand =
|
|
1002
|
+
const editCommand = buildSafeCommand('edit', [
|
|
1003
|
+
'item',
|
|
1004
|
+
id,
|
|
1005
|
+
itemBase64,
|
|
1006
|
+
]);
|
|
887
1007
|
const result = await executeCliCommand(editCommand);
|
|
888
1008
|
return {
|
|
889
1009
|
content: [
|