harden.plugin.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  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. * :::caution
  85. * Note: By default, if the "take" argument is omitted from a list query (e.g. the `products` or `collections` query), a default factor of 1000 is applied.
  86. * :::
  87. *
  88. * The optimal max complexity score will vary depending on:
  89. *
  90. * - The requirements of your storefront and other clients using the Shop API
  91. * - The resources available to your server
  92. *
  93. * You should aim to set the maximum as low as possible while still being able to service all the requests required.
  94. * This will take some manual tuning.
  95. * 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
  96. * that total score is derived from its child fields:
  97. *
  98. * @example
  99. * ```ts
  100. * import { HardenPlugin } from '\@vendure/harden-plugin';
  101. *
  102. * const config: VendureConfig = {
  103. * // A detailed summary is logged at the "debug" level
  104. * logger: new DefaultLogger({ level: LogLevel.Debug }),
  105. * plugins: [
  106. * HardenPlugin.init({
  107. * maxQueryComplexity: 650,
  108. * logComplexityScore: true,
  109. * }),
  110. * ],
  111. * };
  112. * ```
  113. *
  114. * With logging configured as above, the following query:
  115. *
  116. * ```GraphQL
  117. * query ProductList {
  118. * products(options: { take: 5 }) {
  119. * items {
  120. * id
  121. * name
  122. * featuredAsset {
  123. * preview
  124. * }
  125. * }
  126. * }
  127. * }
  128. * ```
  129. * will log the following breakdown:
  130. *
  131. * ```sh
  132. * debug 16/12/22, 14:12 - [HardenPlugin] Calculating complexity of [ProductList]
  133. * debug 16/12/22, 14:12 - [HardenPlugin] Product.id: ID! childComplexity: 0, score: 1
  134. * debug 16/12/22, 14:12 - [HardenPlugin] Product.name: String! childComplexity: 0, score: 1
  135. * debug 16/12/22, 14:12 - [HardenPlugin] Asset.preview: String! childComplexity: 0, score: 1
  136. * debug 16/12/22, 14:12 - [HardenPlugin] Product.featuredAsset: Asset childComplexity: 1, score: 2
  137. * debug 16/12/22, 14:12 - [HardenPlugin] ProductList.items: [Product!]! childComplexity: 4, score: 20
  138. * debug 16/12/22, 14:12 - [HardenPlugin] Query.products: ProductList! childComplexity: 20, score: 35
  139. * verbose 16/12/22, 14:12 - [HardenPlugin] Query complexity [ProductList]: 35
  140. * ```
  141. *
  142. * @docsCategory core plugins/HardenPlugin
  143. */
  144. @VendurePlugin({
  145. providers: [
  146. {
  147. provide: HARDEN_PLUGIN_OPTIONS,
  148. useFactory: () => HardenPlugin.options,
  149. },
  150. ],
  151. configuration: config => {
  152. if (HardenPlugin.options.hideFieldSuggestions !== false) {
  153. Logger.verbose('Configuring HideValidationErrorsPlugin', loggerCtx);
  154. config.apiOptions.apolloServerPlugins.push(new HideValidationErrorsPlugin());
  155. }
  156. config.apiOptions.apolloServerPlugins.push(new QueryComplexityPlugin(HardenPlugin.options));
  157. if (HardenPlugin.options.apiMode !== 'dev') {
  158. config.apiOptions.adminApiDebug = false;
  159. config.apiOptions.shopApiDebug = false;
  160. config.apiOptions.introspection = false;
  161. }
  162. return config;
  163. },
  164. compatibility: '^3.0.0',
  165. })
  166. export class HardenPlugin {
  167. static options: HardenPluginOptions;
  168. static init(options: HardenPluginOptions) {
  169. this.options = options;
  170. return HardenPlugin;
  171. }
  172. }