Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

142 rindas
4.6KB

  1. namespace Campaign_Tracker.Server.LegacyData;
  2. /// <summary>
  3. /// Guards the anti-corruption layer boundary against write SQL being executed
  4. /// on legacy Access tables (AC #2).
  5. ///
  6. /// Any concrete implementation that executes raw SQL must call
  7. /// <see cref="Validate"/> before sending the command, so the layer boundary
  8. /// remains enforceable even when raw ADO.NET is used.
  9. /// </summary>
  10. public static class ReadOnlyCommandGuard
  11. {
  12. private static readonly string[] WriteKeywords =
  13. [
  14. "INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER",
  15. "TRUNCATE", "EXEC", "EXECUTE", "MERGE", "REPLACE",
  16. ];
  17. /// <summary>
  18. /// Validates that <paramref name="sql"/> is a SELECT-only statement.
  19. /// Throws <see cref="LegacyWriteAttemptException"/> if any write keyword is detected.
  20. /// </summary>
  21. public static void Validate(string sql)
  22. {
  23. if (string.IsNullOrWhiteSpace(sql))
  24. {
  25. throw new LegacyWriteAttemptException("Empty SQL is not permitted on legacy tables.");
  26. }
  27. var normalised = sql.Trim();
  28. var commandText = StripNonExecutableText(normalised);
  29. if (!commandText.TrimStart().StartsWith("SELECT", StringComparison.OrdinalIgnoreCase))
  30. {
  31. throw new LegacyWriteAttemptException(
  32. $"Only SELECT statements are permitted on legacy tables. Received: {Truncate(normalised)}");
  33. }
  34. if (ContainsAdditionalStatement(commandText))
  35. {
  36. throw new LegacyWriteAttemptException("Multiple SQL statements are not permitted on legacy tables.");
  37. }
  38. foreach (var keyword in WriteKeywords)
  39. {
  40. if (ContainsWordBoundary(commandText, keyword))
  41. {
  42. throw new LegacyWriteAttemptException(
  43. $"Write keyword '{keyword}' detected in legacy table query. Statement blocked.");
  44. }
  45. }
  46. }
  47. private static bool ContainsWordBoundary(string sql, string keyword)
  48. {
  49. var index = -1;
  50. while ((index = sql.IndexOf(keyword, index + 1, StringComparison.OrdinalIgnoreCase)) >= 0)
  51. {
  52. var before = index == 0 || !IsIdentifierCharacter(sql[index - 1]);
  53. var after = index + keyword.Length >= sql.Length
  54. || !IsIdentifierCharacter(sql[index + keyword.Length]);
  55. if (before && after)
  56. {
  57. return true;
  58. }
  59. }
  60. return false;
  61. }
  62. private static bool ContainsAdditionalStatement(string sql)
  63. {
  64. var semicolonIndex = sql.IndexOf(';');
  65. return semicolonIndex >= 0 &&
  66. sql[(semicolonIndex + 1)..].Any(character => !char.IsWhiteSpace(character));
  67. }
  68. private static string StripNonExecutableText(string sql)
  69. {
  70. var result = new char[sql.Length];
  71. var index = 0;
  72. while (index < sql.Length)
  73. {
  74. if (sql[index] == '\'' || sql[index] == '"' || sql[index] == '[')
  75. {
  76. var close = sql[index] == '[' ? ']' : sql[index];
  77. result[index++] = ' ';
  78. while (index < sql.Length)
  79. {
  80. var current = sql[index];
  81. result[index++] = ' ';
  82. if (current == close)
  83. {
  84. break;
  85. }
  86. }
  87. continue;
  88. }
  89. if (sql[index] == '-' && index + 1 < sql.Length && sql[index + 1] == '-')
  90. {
  91. result[index++] = ' ';
  92. result[index++] = ' ';
  93. while (index < sql.Length && sql[index] != '\r' && sql[index] != '\n')
  94. {
  95. result[index++] = ' ';
  96. }
  97. continue;
  98. }
  99. if (sql[index] == '/' && index + 1 < sql.Length && sql[index + 1] == '*')
  100. {
  101. result[index++] = ' ';
  102. result[index++] = ' ';
  103. while (index < sql.Length)
  104. {
  105. var current = sql[index];
  106. result[index++] = ' ';
  107. if (current == '*' && index < sql.Length && sql[index] == '/')
  108. {
  109. result[index++] = ' ';
  110. break;
  111. }
  112. }
  113. continue;
  114. }
  115. result[index] = sql[index];
  116. index++;
  117. }
  118. return new string(result);
  119. }
  120. private static bool IsIdentifierCharacter(char character) =>
  121. char.IsLetterOrDigit(character) || character == '_';
  122. private static string Truncate(string sql) =>
  123. sql.Length > 60 ? sql[..60] + "..." : sql;
  124. }

Powered by TurnKey Linux.