Skip to content

Commit 5764fec

Browse files
committed
fix: address PR review feedback for executeCommand bridge
- Extract <head> tag string to constant in InjectBridgeScriptIntoHtml - Move callbackId validation outside ThreadHelper.Run in InvokeCommandCallback - Extract JS string escaping into ExecuteCommandBridge.EscapeForJsString utility - Add _cb_8 and whitespace test cases for IsValidCallbackId - Add test for default OAuth fallback on invalid auth method string
1 parent 1e8502e commit 5764fec

4 files changed

Lines changed: 34 additions & 11 deletions

File tree

Snyk.VisualStudio.Extension.2022/Settings/HtmlSettingsWindow.xaml.cs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,11 @@ protected virtual string InjectBridgeScriptIntoHtml(string html)
187187
var script = BuildIdeBridgeScript();
188188
var scriptTag = $"<script type=\"text/javascript\">{script}</script>";
189189

190-
var headIndex = html.IndexOf("<head>", StringComparison.OrdinalIgnoreCase);
190+
const string headTag = "<head>";
191+
var headIndex = html.IndexOf(headTag, StringComparison.OrdinalIgnoreCase);
191192
if (headIndex >= 0)
192193
{
193-
return html.Insert(headIndex + "<head>".Length, scriptTag);
194+
return html.Insert(headIndex + headTag.Length, scriptTag);
194195
}
195196

196197
// Fallback: prepend if no <head> found
@@ -291,6 +292,12 @@ public void UpdateAuthToken(string token, string apiUrl = null)
291292

292293
private void InvokeCommandCallback(string callbackId, string resultJson)
293294
{
295+
if (!ExecuteCommandBridge.IsValidCallbackId(callbackId))
296+
{
297+
Logger.Warning("Rejected callbackId with unexpected format: {CallbackId}", callbackId);
298+
return;
299+
}
300+
294301
try
295302
{
296303
ThreadHelper.JoinableTaskFactory.Run(async () =>
@@ -300,13 +307,7 @@ private void InvokeCommandCallback(string callbackId, string resultJson)
300307
dynamic doc = SettingsBrowser.Document;
301308
if (doc == null) return;
302309

303-
if (!ExecuteCommandBridge.IsValidCallbackId(callbackId))
304-
{
305-
Logger.Warning("Rejected callbackId with unexpected format: {CallbackId}", callbackId);
306-
return;
307-
}
308-
309-
var escapedCallbackId = callbackId.Replace("\\", "\\\\").Replace("'", "\\'");
310+
var escapedCallbackId = ExecuteCommandBridge.EscapeForJsString(callbackId);
310311
var script = $"if(window.__ideCallbacks__&&window.__ideCallbacks__['{escapedCallbackId}']){{" +
311312
$"var cb=window.__ideCallbacks__['{escapedCallbackId}'];" +
312313
$"delete window.__ideCallbacks__['{escapedCallbackId}'];" +
@@ -336,8 +337,8 @@ private void InvokeSetAuthToken(string token, string apiUrl)
336337
return;
337338
}
338339

339-
var escapedToken = token.Replace("\\", "\\\\").Replace("'", "\\'");
340-
var escapedApiUrl = (apiUrl ?? string.Empty).Replace("\\", "\\\\").Replace("'", "\\'");
340+
var escapedToken = ExecuteCommandBridge.EscapeForJsString(token);
341+
var escapedApiUrl = ExecuteCommandBridge.EscapeForJsString(apiUrl ?? string.Empty);
341342
var script = $@"
342343
(function() {{
343344
if (typeof window.setAuthToken === 'function') {{

Snyk.VisualStudio.Extension.2022/UI/Html/ExecuteCommandBridge.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ public static class ExecuteCommandBridge
3030
// Allowlist regex for callbackIds produced by BuildClientScript: "" or "__cb_<digits>"
3131
private static readonly Regex CallbackIdPattern = new Regex(@"^(__cb_\d+)?$", RegexOptions.Compiled);
3232

33+
/// <summary>
34+
/// Escapes a string for safe embedding inside a single-quoted JavaScript string literal.
35+
/// Handles backslashes first, then single quotes, to avoid double-escaping.
36+
/// </summary>
37+
public static string EscapeForJsString(string value) =>
38+
(value ?? string.Empty).Replace("\\", "\\\\").Replace("'", "\\'");
39+
3340
/// <summary>
3441
/// Returns true if <paramref name="callbackId"/> matches the expected format produced by
3542
/// <see cref="BuildClientScript"/>. Used as an XSS guard before injecting the id back into JS.

Snyk.VisualStudio.Extension.Tests/UI/Html/ExecuteCommandBridgeTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public void IsValidCallbackId_AcceptsValidFormats(string callbackId, bool expect
6262
[InlineData(";drop table--")]
6363
[InlineData("__cb_1; alert(1)")]
6464
[InlineData("__cb_")]
65+
[InlineData("_cb_8")]
66+
[InlineData(" ")]
6567
public void IsValidCallbackId_RejectsInvalidFormats(string callbackId)
6668
{
6769
Assert.False(ExecuteCommandBridge.IsValidCallbackId(callbackId));

Snyk.VisualStudio.Extension.Tests/UI/Html/HtmlSettingsScriptingBridgeTest.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,19 @@ public void IdeExecuteCommand_SnykLogin_SavesTokenMethod()
6464
optionsMock.VerifySet(o => o.AuthenticationMethod = AuthenticationType.Token);
6565
}
6666

67+
[Theory]
68+
[InlineData("unknown")]
69+
[InlineData("")]
70+
[InlineData(" ")]
71+
public void IdeExecuteCommand_SnykLogin_DefaultsToOAuthForInvalidMethod(string authMethod)
72+
{
73+
var args = JsonConvert.SerializeObject(new object[] { authMethod, "https://api.snyk.io", false });
74+
75+
bridge.__ideExecuteCommand__("snyk.login", args, "");
76+
77+
optionsMock.VerifySet(o => o.AuthenticationMethod = AuthenticationType.OAuth);
78+
}
79+
6780
[Fact]
6881
public void IdeExecuteCommand_SnykLogin_SavesEndpoint()
6982
{

0 commit comments

Comments
 (0)