Skip to content

Commit 239955c

Browse files
authored
Filter sensitive history items and avoid writing them to the history file (#1058)
1 parent 2e12332 commit 239955c

File tree

5 files changed

+328
-32
lines changed

5 files changed

+328
-32
lines changed

PSReadLine/Cmdlets.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ public enum HistorySaveStyle
5757
SaveNothing
5858
}
5959

60+
public enum AddToHistoryOption
61+
{
62+
SkipAdding,
63+
MemoryOnly,
64+
MemoryAndFile
65+
}
66+
6067
public class PSConsoleReadLineOptions
6168
{
6269
public const ConsoleColor DefaultCommentColor = ConsoleColor.DarkGreen;
@@ -135,7 +142,7 @@ public PSConsoleReadLineOptions(string hostName)
135142
ContinuationPrompt = DefaultContinuationPrompt;
136143
ContinuationPromptColor = Console.ForegroundColor;
137144
ExtraPromptLineCount = DefaultExtraPromptLineCount;
138-
AddToHistoryHandler = null;
145+
AddToHistoryHandler = DefaultAddToHistoryHandler;
139146
HistoryNoDuplicates = DefaultHistoryNoDuplicates;
140147
MaximumHistoryCount = DefaultMaximumHistoryCount;
141148
MaximumKillRingCount = DefaultMaximumKillRingCount;
@@ -237,10 +244,12 @@ public object ContinuationPromptColor
237244

238245
/// <summary>
239246
/// This handler is called before adding a command line to history.
240-
/// The return value indicates if the command line should be added
241-
/// to history or not.
247+
/// The return value indicates if the command line should be skipped,
248+
/// or added to memory only, or added to both memory and history file.
242249
/// </summary>
243-
public Func<string, bool> AddToHistoryHandler { get; set; }
250+
public Func<string, object> AddToHistoryHandler { get; set; }
251+
public static readonly Func<string, object> DefaultAddToHistoryHandler =
252+
s => PSConsoleReadLine.GetDefaultAddToHistoryOption(s);
244253

245254
/// <summary>
246255
/// This handler is called from ValidateAndAcceptLine.
@@ -514,7 +523,7 @@ public SwitchParameter HistoryNoDuplicates
514523

515524
[Parameter]
516525
[AllowNull]
517-
public Func<string, bool> AddToHistoryHandler
526+
public Func<string, object> AddToHistoryHandler
518527
{
519528
get => _addToHistoryHandler;
520529
set
@@ -523,7 +532,7 @@ public Func<string, bool> AddToHistoryHandler
523532
_addToHistoryHandlerSpecified = true;
524533
}
525534
}
526-
private Func<string, bool> _addToHistoryHandler;
535+
private Func<string, object> _addToHistoryHandler;
527536
internal bool _addToHistoryHandlerSpecified;
528537

529538
[Parameter]

PSReadLine/History.cs

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
using System.Diagnostics;
88
using System.IO;
99
using System.Linq;
10+
using System.Management.Automation;
1011
using System.Text;
12+
using System.Text.RegularExpressions;
1113
using System.Threading;
1214
using Microsoft.PowerShell.PSReadLine;
1315

@@ -51,6 +53,7 @@ public class HistoryItem
5153
public bool FromHistoryFile { get; internal set; }
5254

5355
internal bool _saved;
56+
internal bool _sensitive;
5457
internal List<EditItem> _edits;
5558
internal int _undoEditIndex;
5659
}
@@ -76,29 +79,60 @@ public class HistoryItem
7679
private const string _failedForwardISearchPrompt = "failed-fwd-i-search: ";
7780
private const string _failedBackwardISearchPrompt = "failed-bck-i-search: ";
7881

79-
private string MaybeAddToHistory(
80-
string result,
81-
List<EditItem> edits,
82-
int undoEditIndex,
83-
bool fromDifferentSession = false,
84-
bool fromInitialRead = false)
82+
// Pattern used to check for sensitive inputs.
83+
private static readonly Regex s_sensitivePattern = new Regex(
84+
"password|asplaintext|token|key|secret",
85+
RegexOptions.Compiled | RegexOptions.IgnoreCase);
86+
87+
private AddToHistoryOption GetAddToHistoryOption(string line)
8588
{
86-
bool AddToHistory(string line)
89+
// Whitespace only is useless, never add.
90+
if (string.IsNullOrWhiteSpace(line))
91+
{
92+
return AddToHistoryOption.SkipAdding;
93+
}
94+
95+
// Under "no dupes" (which is on by default), immediately drop dupes of the previous line.
96+
if (Options.HistoryNoDuplicates && _history.Count > 0 &&
97+
string.Equals(_history[_history.Count - 1].CommandLine, line, StringComparison.Ordinal))
8798
{
88-
// Whitespace only is useless, never add.
89-
if (string.IsNullOrWhiteSpace(line)) return false;
99+
return AddToHistoryOption.SkipAdding;
100+
}
90101

91-
// If the user says don't add it, then don't.
92-
if (Options.AddToHistoryHandler != null && !Options.AddToHistoryHandler(result)) return false;
102+
if (Options.AddToHistoryHandler != null)
103+
{
104+
if (Options.AddToHistoryHandler == PSConsoleReadLineOptions.DefaultAddToHistoryHandler)
105+
{
106+
// Avoid boxing if it's the default handler.
107+
return GetDefaultAddToHistoryOption(line);
108+
}
93109

94-
// Under "no dupes" (which is on by default), immediately drop dupes of the previous line.
95-
if (Options.HistoryNoDuplicates && _history.Count > 0)
96-
return !string.Equals(_history[_history.Count - 1].CommandLine, result, StringComparison.Ordinal);
110+
object value = Options.AddToHistoryHandler(line);
97111

98-
return true;
112+
if (LanguagePrimitives.TryConvertTo(value, out AddToHistoryOption enumValue))
113+
{
114+
return enumValue;
115+
}
116+
117+
if (value is bool boolValue && !boolValue)
118+
{
119+
return AddToHistoryOption.SkipAdding;
120+
}
99121
}
100122

101-
if (AddToHistory(result))
123+
// Add to both history queue and file by default.
124+
return AddToHistoryOption.MemoryAndFile;
125+
}
126+
127+
private string MaybeAddToHistory(
128+
string result,
129+
List<EditItem> edits,
130+
int undoEditIndex,
131+
bool fromDifferentSession = false,
132+
bool fromInitialRead = false)
133+
{
134+
var addToHistoryOption = GetAddToHistoryOption(result);
135+
if (addToHistoryOption != AddToHistoryOption.SkipAdding)
102136
{
103137
var fromHistoryFile = fromDifferentSession || fromInitialRead;
104138
_previousHistoryItem = new HistoryItem
@@ -110,14 +144,16 @@ bool AddToHistory(string line)
110144
FromOtherSession = fromDifferentSession,
111145
FromHistoryFile = fromInitialRead,
112146
};
147+
113148
if (!fromHistoryFile)
114149
{
150+
// 'MemoryOnly' indicates sensitive content in the command line
151+
_previousHistoryItem._sensitive = addToHistoryOption == AddToHistoryOption.MemoryOnly;
115152
_previousHistoryItem.StartTime = DateTime.UtcNow;
116153
}
117154

118155
_history.Enqueue(_previousHistoryItem);
119156

120-
121157
_currentHistoryIndex = _history.Count;
122158

123159
if (_options.HistorySaveStyle == HistorySaveStyle.SaveIncrementally && !fromHistoryFile)
@@ -169,7 +205,6 @@ private void SaveHistoryAtExit()
169205
WriteHistoryRange(0, _history.Count - 1, File.CreateText);
170206
}
171207

172-
173208
private int historyErrorReportedCount;
174209
private void ReportHistoryFileError(Exception e)
175210
{
@@ -241,8 +276,13 @@ private void WriteHistoryRange(int start, int end, Func<string, StreamWriter> fi
241276
{
242277
for (var i = start; i <= end; i++)
243278
{
244-
_history[i]._saved = true;
245-
var line = _history[i].CommandLine.Replace("\n", "`\n");
279+
HistoryItem item = _history[i];
280+
item._saved = true;
281+
282+
// Actually, skip writing sensitive items to file.
283+
if (item._sensitive) { continue; }
284+
285+
var line = item.CommandLine.Replace("\n", "`\n");
246286
file.WriteLine(line);
247287
}
248288
}
@@ -335,6 +375,13 @@ void UpdateHistoryFromFile(IEnumerable<string> historyLines, bool fromDifferentS
335375
}
336376
}
337377

378+
public static AddToHistoryOption GetDefaultAddToHistoryOption(string line)
379+
{
380+
return s_sensitivePattern.IsMatch(line)
381+
? AddToHistoryOption.MemoryOnly
382+
: AddToHistoryOption.MemoryAndFile;
383+
}
384+
338385
/// <summary>
339386
/// Add a command to the history - typically used to restore
340387
/// history from a previous session.

docs/Set-PSReadLineOption.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Set-PSReadLineOption
2121
[-PromptText <string>]
2222
[-ExtraPromptLineCount <Int32>]
2323
[-Colors <Hashtable>]
24-
[-AddToHistoryHandler <Func[String, Boolean]>]
24+
[-AddToHistoryHandler <Func[String, Object]>]
2525
[-CommandValidationHandler <Action[CommandAst]>]
2626
[-ContinuationPrompt <String>]
2727
[-HistorySearchCursorMovesToEnd]
@@ -192,13 +192,34 @@ Accept wildcard characters: False
192192
193193
### -AddToHistoryHandler
194194
195-
Specifies a ScriptBlock that can be used to control which commands get added to PSReadLine history.
195+
Specifies a ScriptBlock that can be used to control which commands get added to PSReadLine history,
196+
and whether they should be saved to the history file.
197+
198+
The ScriptBlock is passed the command line, and it is expected to return either a Boolean value,
199+
or an enum value of the type `[Microsoft.PowerShell.AddToHistoryOption]`.
200+
The enum type `AddToHistoryOption` has 3 members: `SkipAdding`, `MemoryOnly`, and `MemoryAndFile`.
201+
202+
If the ScriptBlock returns `$true`, it's equivalent to `AddToHistoryOption.MemoryAndFile`.
203+
The command line is added to the in-memory history queue and saved to the history file.
204+
If the ScriptBlock returns `$false`, it's equivalent to `AddToHistoryOption.SkipAdding`,
205+
and the command line is not added to history at all.
206+
207+
If the ScriptBlock returns `AddToHistoryOption.MemoryOnly`, then the command line is added to the in-memory history queue,
208+
but will not be saved to the history file.
209+
This usually indicates the command line has sensitive content that should not be written to disk.
210+
211+
PSReadLine provides a default handler to this option:
212+
`[Microsoft.PowerShell.PSConsoleReadLine]::GetDefaultAddToHistoryOption(string line)`
213+
The default handler attempts to detect sensitive information in a command line by matching with a simple regex pattern:
214+
`"password|asplaintext|token|key|secret"`
215+
When successfully matched, the command line is considered to contain sensitive content, and `MemoryOnly` is returned.
216+
Otherwise, `MemoryAndFile` is returned.
217+
218+
To turn off the default handler, just set this option to `$null`.
196219

197-
The ScriptBlock is passed the command line.
198-
If the ScriptBlock returns `$true`, the command line is added to history, otherwise it is not.
199220

200221
```yaml
201-
Type: Func[String, Boolean]
222+
Type: Func[String, Object]
202223
Parameter Sets: (All)
203224
Aliases:
204225

0 commit comments

Comments
 (0)