harden.plugin.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import { Logger, VendurePlugin } from '@vendure/core';
  2. import { HARDEN_PLUGIN_OPTIONS, loggerCtx } from './constants';
  3. import { HideValidationErrorsPlugin } from './middleware/hide-validation-errors-plugin';
  4. import { QueryComplexityPlugin } from './middleware/query-complexity-plugin';
  5. import { HardenPluginOptions } from './types';
  6. /**
  7. * @description
  8. * The HardenPlugin hardens the Shop and Admin GraphQL APIs against attacks and abuse.
  9. *
  10. * - It analyzes the complexity on incoming graphql queries and rejects queries that are too complex and
  11. * could be used to overload the resources of the server.
  12. * - It disables dev-mode API features such as introspection and the GraphQL playground app.
  13. * - It removes field name suggestions to prevent trial-and-error schema sniffing.
  14. *
  15. * It is a recommended plugin for all production configurations.
  16. *
  17. * ## Installation
  18. *
  19. * `yarn add \@vendure/harden-plugin`
  20. *
  21. * or
  22. *
  23. * `npm install \@vendure/harden-plugin`
  24. *
  25. * Then add the `HardenPlugin`, calling the `.init()` method with {@link HardenPluginOptions}:
  26. *
  27. * @example
  28. * ```ts
  29. * import { HardenPlugin } from '\@vendure/harden-plugin';
  30. *
  31. * const config: VendureConfig = {
  32. * // Add an instance of the plugin to the plugins array
  33. * plugins: [
  34. * HardenPlugin.init({
  35. * maxQueryComplexity: 650,
  36. * apiMode: process.env.APP_ENV === 'dev' ? 'dev' : 'prod',
  37. * }),
  38. * ],
  39. * };
  40. * ```
  41. *
  42. * ## Setting the max query complexity
  43. *
  44. * The `maxQueryComplexity` option determines how complex a query can be. The complexity of a query relates to how many, and how
  45. * deeply-nested are the fields being selected, and is intended to roughly correspond to the amount of server resources that would
  46. * be required to resolve that query.
  47. *
  48. * The goal of this setting is to prevent attacks in which a malicious actor crafts a very complex query in order to overwhelm your
  49. * server resources. Here's an example of a request which would likely overwhelm a Vendure server:
  50. *
  51. * ```GraphQL
  52. * query EvilQuery {
  53. * products {
  54. * items {
  55. * collections {
  56. * productVariants {
  57. * items {
  58. * product {
  59. * collections {
  60. * productVariants {
  61. * items {
  62. * product {
  63. * variants {
  64. * name
  65. * }
  66. * }
  67. * }
  68. * }
  69. * }
  70. * }
  71. * }
  72. * }
  73. * }
  74. * }
  75. * }
  76. * }
  77. * ```
  78. *
  79. * This evil query has a complexity score of 2,443,203 - much greater than the default of 1,000!
  80. *
  81. * The complexity score is calculated by the [graphql-query-complexity library](https://www.npmjs.com/package/graphql-query-complexity),
  82. * and by default uses the {@link defaultVendureComplexityEstimator}, which is tuned specifically to the Vendure Shop API.
  83. *
  84. * The optimal max complexity score will vary depending on:
  85. *
  86. * - The requirements of your storefront and other clients using the Shop API
  87. * - The resources available to your server
  88. *
  89. * You should aim to set the maximum as low as possible while still being able to service all the requests required.
  90. * This will take some manual tuning.
  91. * While tuning the max, you can turn on the `logComplexityScore` to get a detailed breakdown of the complexity of each query, as well as how
  92. * that total score is derived from its child fields:
  93. *
  94. * @example
  95. * ```ts
  96. * import { HardenPlugin } from '\@vendure/harden-plugin';
  97. *
  98. * const config: VendureConfig = {
  99. * // A detailed summary is logged at the "debug" level
  100. * logger: new DefaultLogger({ level: LogLevel.Debug }),
  101. * plugins: [
  102. * HardenPlugin.init({
  103. * maxQueryComplexity: 650,
  104. * logComplexityScore: true,
  105. * }),
  106. * ],
  107. * };
  108. * ```
  109. *
  110. * With logging configured as above, the following query:
  111. *
  112. * ```GraphQL
  113. * query ProductList {
  114. * products(options: { take: 5 }) {
  115. * items {
  116. * id
  117. * name
  118. * featuredAsset {
  119. * preview
  120. * }
  121. * }
  122. * }
  123. * }
  124. * ```
  125. * will log the following breakdown:
  126. *
  127. * ```sh
  128. * debug 16/12/22, 14:12 - [HardenPlugin] Calculating complexity of [ProductList]
  129. * debug 16/12/22, 14:12 - [HardenPlugin] Product.id: ID! childComplexity: 0, score: 1
  130. * debug 16/12/22, 14:12 - [HardenPlugin] Product.name: String! childComplexity: 0, score: 1
  131. * debug 16/12/22, 14:12 - [HardenPlugin] Asset.preview: String! childComplexity: 0, score: 1
  132. * debug 16/12/22, 14:12 - [HardenPlugin] Product.featuredAsset: Asset childComplexity: 1, score: 2
  133. * debug 16/12/22, 14:12 - [HardenPlugin] ProductList.items: [Product!]! childComplexity: 4, score: 20
  134. * debug 16/12/22, 14:12 - [HardenPlugin] Query.products: ProductList! childComplexity: 20, score: 35
  135. * verbose 16/12/22, 14:12 - [HardenPlugin] Query complexity [ProductList]: 35
  136. * ```
  137. *
  138. * @docsCategory HardenPlugin
  139. */
  140. @VendurePlugin({
  141. providers: [
  142. {
  143. provide: HARDEN_PLUGIN_OPTIONS,
  144. useFactory: () => HardenPlugin.options,
  145. },
  146. ],
  147. configuration: config => {
  148. if (HardenPlugin.options.hideFieldSuggestions !== false) {
  149. Logger.verbose('Configuring HideValidationErrorsPlugin', loggerCtx);
  150. config.apiOptions.apolloServerPlugins.push(new HideValidationErrorsPlugin());
  151. }
  152. config.apiOptions.apolloServerPlugins.push(new QueryComplexityPlugin(HardenPlugin.options));
  153. if (HardenPlugin.options.apiMode !== 'dev') {
  154. config.apiOptions.adminApiDebug = false;
  155. config.apiOptions.shopApiDebug = false;
  156. config.apiOptions.introspection = false;
  157. }
  158. return config;
  159. },
  160. compatibility: '^2.0.0-beta.0',
  161. })
  162. export class HardenPlugin {
  163. static options: HardenPluginOptions;
  164. static init(options: HardenPluginOptions) {
  165. this.options = options;
  166. return HardenPlugin;
  167. }
  168. }