【问题标题】:SHA256 webhook signature from WooCommerce never verifiesWooCommerce 的 SHA256 webhook 签名从不验证
【发布时间】:2015-11-14 08:39:21
【问题描述】:

我从 woocommerce 网站接收 webhook 到 nodejs/express 应用程序。我正在尝试验证 webhook 的签名以证明其真实性,但我计算的哈希与 woocommerce 在 hook 的签名标头中报告的签名不对应。

这是我用来验证真实性的代码:

function verifySignature(signature, payload, key){     
    var computedSignature = crypto.createHmac("sha256", key).update(payload).digest('base64');
    debug('computed signature: %s', computedSignature);
    return computedSignature === signature;
  }

使用以下参数调用此函数:

var signature = req.headers['x-wc-webhook-signature'];
verifySignature(signature, JSON.stringify(req.body), config.wooCommence.accounts.api[config.env].webhookSecret)

webhook 的签名标头将签名报告为BewIV/zZMbmuJkHaUwaQxjX8yR6jRktPZQN9j2+67Oo=。然而,上述操作的结果是S34YqftH1R8F4uH4Ya2BSM1rn0H9NiqEA2Nr7W1CWZs=

我已经在 webhook 上手动配置了密码,正如您在上面的代码中看到的,同样的密码也在 express 应用程序中被硬编码。因此,要么我使用错误的有效负载来计算签名,要么有其他可疑的东西阻止我验证这些签名。

如果有任何建议可以帮助我解决这个问题。

【问题讨论】:

  • 以下 Johannes 的回答应该被接受。遵循他的方法对我来说非常有效。一旦我把它清理干净,我将发布一个 ASP.net 解决方案。

标签: node.js hash woocommerce webhooks


【解决方案1】:

老问题,但也许它可以帮助一些可怜的灵魂。 签名需要根据正文而不是它包含的 JSON 进行检查。即正文的原始字节。

伪:

        byte[] body = request.Body;
        string signature = request.Header["X-WC-Webhook-Signature"];

        byte[] secretUtf8 = GetUtf8Bytes("yoursecrethere");
        byte[] hash = HMAC_SHA256.ComputeHash(body, secretUtf8);
        string hashBase64 = ToBase64String(hash);

        bool isValid = hashBase64 == signature;

【讨论】:

    【解决方案2】:

    我在寻找一种解决方案时偶然发现了这一点,该解决方案可以让 Asp.NET 应用程序检查 Woocommerce 网络挂钩的签名。我的回答是基于 Johannes 提供的伪代码,效果很好。我实现了一个自定义控制器属性来拦截请求并在签名到达 API 控制器方法之前检查它:

    public class HmacSignatureFilter : ActionFilterAttribute
    {
    
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            var requestContent = actionContext.Request.Content;
            var jsonContent = requestContent.ReadAsStringAsync().Result;
            var byteContent = requestContent.ReadAsByteArrayAsync().Result;
    
            //if the request contains this, it's the verification request from Woocommerce
            //when the webhook is created so let it pass through so it can be verified
            if (!jsonContent.Contains("webhook_id"))
            {
                var requestSignature = actionContext.Request.Headers;
    
                var bodyHash = HashHMAC("test", byteContent); //this is the shared key between Woo and custom API.  should be from config or database table.
    
                var signature = actionContext.Request.Headers.GetValues("x-wc-webhook-signature");
    
                if (bodyHash != signature.FirstOrDefault())
                {
                    throw new HttpResponseException(HttpStatusCode.Forbidden);
                }
            }
    
            base.OnActionExecuting(actionContext);
        }
    
    
        private static string HashHMAC(string key, byte[] message)
        {
            var keyBytes = Encoding.UTF8.GetBytes(key);
            var hash = new HMACSHA256(keyBytes);
    
            var computedHash = hash.ComputeHash(message);
            return Convert.ToBase64String(computedHash);
        }
    }
    

    然后在您的 Api 控制器中使用过滤器:

    [RoutePrefix("api/woo")]
    public class WooController : ApiController
    {
    
        public SomeService _service;
    
        public WooController()
        {
            this._service = new SomeService();
        }
    
        // POST api/values
        [Route("orderCreated")]
        [HttpPost]
        [HmacSignatureFilter]
        public string Post()
        {
            var requestContent = Request.Content;
            var jsonContent = requestContent.ReadAsStringAsync().Result;
    
            //this is the test request from Woocommerce.  Don't do anything but 
            //respond so it can verify the endpoint
            if (jsonContent.Contains("webhook_id"))
            {
                return "Webhook Test Success";
            }
    
            var wooOrder = JsonConvert.DeserializeObject<WooOrderModel>(jsonContent);
    
            //call a service to use the order data provided by WooCommerce
            _service.AddOrder(wooOrder);
    
            return "Success";
        }
    
    }
    

    注意:哈希码引用自this SO post

    【讨论】:

    • 我必须这样做才能读取内容:string jsonContent;字节 [] 字节内容 = 空;使用 (var stream = new StreamReader(actionContext.Request.Content.ReadAsStreamAsync().Result)) { stream.BaseStream.Position = 0; jsonContent = stream.ReadToEnd();流.BaseStream.Position = 0;使用 (var memstream = new MemoryStream()) { stream.BaseStream.CopyTo(memstream); byteContent = memstream.ToArray(); } }
    【解决方案3】:

    对于使用节点的人来说,这应该可以解决问题。

    var processWebHookSignature = function (secret, body, signature) {
      signatureComputed = crypto.createHmac('SHA256', secret).update(
        new Buffer(JSON.stringify(body), 'utf8')).digest('base64');
    
      return ( signatureComputed === signature ) ? true : false;
    }
    

    【讨论】:

    【解决方案4】:

    必须在“原始主体”上计算哈希。当在“快速应用程序”中使用并使用 JSON bodyParser 中间件时,“原始主体”会丢失,请参阅 How to access the raw body of the request before bodyparser? 以保留“原始主体”。

    例如:

    // 'misuse' verify option  
    app.use(bodyParser.json({
      verify: function(req,res,buf) { 
        req.rawBody=buf; 
      }
    }));
    
    var wcSignature = req.get('X-Wc-Webhook-Signature');
    debug('wc signature: %s', wcSignature);
    var calculatedSignature = crypto.createHmac('SHA256', secret)
      .update(req.rawBody, 'utf8')
      .digest('base64');
    debug('calculated signature: %s', calculatedSignature);
    

    【讨论】:

      【解决方案5】:

      希望能节省一些时间,下面对我有用。

      // Make sure to add a WISTIA_SECRET_KEY in your Environment Variables
      // See https://docs.pipedream.com/environment-variables/
      const secret = process.env.SELF_AUTOMATE_KEY;
      const signature = event.headers["x-wc-webhook-signature"];
      const body = steps.trigger.raw_event["body_b64"];
      const clean_Body = body.replace("body_b64: ", "");
      //const body = steps.trigger.raw_event;
      console.log(event.headers["x-wc-webhook-signature"]);
      
      console.log("Print Body", clean_Body);
      
      if (process.env.SELF_AUTOMATE_KEY === undefined) {
        $end("No WISTIA_SECRET_KEY environment variable defined. Exiting.")
      }
      
      if (!("x-wc-webhook-signature" in event.headers)) {
        $end("No x-wc-webhook-signature header present in the request. Exiting.")
      }
      
      // Once we've confirmed we have a signature, we want to 
      // validate it by generating an HMAC SHA-256 hexdigest
      const crypto = require('crypto');
      
      const hash = crypto.createHmac('sha256',
        secret).update(JSON.stringify(clean_Body), 'base64').digest('base64');
      
      
      
      console.log(hash);
      // $end() ends the execution of a pipeline, presenting a nice message in the "Messages"
      // column in the inspector above. See https://docs.pipedream.com/notebook/code/#end
      if (hash !== signature) {
        $end("The correct secret key was not passed in the event. Exiting!")
      }
      

      【讨论】:

        【解决方案6】:

        由于这是该问题的最佳 Google 搜索结果,并且没有完整的答案,这里有一个使用 Flask 验证 WooCommerce webhook 签名的 Python 版本。这需要一些试验和错误,希望它可以帮助那里的人:

        import json
        import base64
        import hmac
        import hashlib
        
        from flask import Flask, request, Response
        
        app = Flask(__name__)
        
        # The WooCommerce webhook secret
        WEBHOOK_SECRET = 'abc123456'
        
        # Function that compares the computed signature to the one in the request
        def verify_woocommerce_signature(body, signature, secret):
            digest = hmac.new(bytes(secret, 'utf-8'), body, hashlib.sha256).digest()
            encoded = base64.b64encode(digest).decode()
        
            return encoded == signature
        
        # WooCommerce Order Creation Event
        @app.route('/webhooks/woocommerce/order_created', methods=['POST'])
        def webhooks_woocommerce_order_created():
            # Get raw request body
            body = request.get_data()
            
            # Get request signature
            signature = request.headers['X-WC-WEBHOOK-SIGNATURE']
            
            # Verify webhook signature and handle mismatch
            if verify_woocommerce_signature(body, signature, WEBHOOK_SECRET) is False:
                msg = {"success": False}
                return Response(json.dumps(msg), status=400, mimetype='application/json')
        
            # Signatures match, process the payload
        

        【讨论】:

        • 像魅力一样工作。谢谢!
        【解决方案7】:

        已在 TypeScript 中解决。我在 server.ts 中添加了这个:

        this.app.use(bodyParser.json({
                    verify: function(req, res, buf) {
                        (req as any).rawBody = buf;
                    }
                }));
        

        比:

         const computedSignature = crypto.createHmac("sha256", process.env.WOOCOMMERCE_SECRET).update((req as any).rawBody).digest("base64");
        

        【讨论】:

          【解决方案8】:

          已在 C# API 中解决。对于将它用作控制器的 C# API 中的属性的人,我在 StackOverflow 上使用多个线程制定了一个很好的解决方案:

          [AttributeUsage(AttributeTargets.Method)]
          public class ShaKeyAttribute : Attribute, IAuthorizationFilter
          {
              private readonly ShaAuthOptions _options;
              private readonly ILogger<ShaKeyAttribute> _log;
          
              public ShaKeyAttribute(IOptions<ShaAuthOptions> options, ILogger<ShaKeyAttribute> log)
              {
                  this._options = options.Value;
                  this._log = log;
              }
          
              public void OnAuthorization(AuthorizationFilterContext context)
              {
                  this._log.LogInformation("Entering in Sha Auth...");
                  if (!context.HttpContext.Request.Headers.TryGetValue("x-wc-webhook-signature", out var extractedSignature))
                  {
                      this._log.LogError("Sha Auth failed. Signature was not provided");
          
                      context.Result = new ContentResult()
                      {
                          StatusCode = 401,
                          Content = "Signature was not provided",
                      };
                      return;
                  }
          
                  // This logic for body rewind comes from: https://stackoverflow.com/a/40994711/14010438
                  string bodyString;
                  var req = context.HttpContext.Request;
          
                  // Allows using several time the stream in ASP.Net Core
                  req.EnableBuffering();
          
                  // Arguments: Stream, Encoding, detect encoding, buffer size
                  // AND, the most important: keep stream opened
                  using (StreamReader reader
                      = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true))
                  {
                      bodyString = reader.ReadToEnd();
                  }
          
                  // Rewind, so the core is not lost when it looks the body for the request
                  req.Body.Position = 0;
          
                  // From this point, DON'T TOUCH THE REQUEST BODY. Instead, use bodyString
                  // https://stackoverflow.com/a/62032738/14010438
                  byte[] requestData = Encoding.UTF8.GetBytes(bodyString);
                  var encoding = new UTF8Encoding();
                  var key = this._options.Secret;
                  var keyBytes = encoding.GetBytes(key);
                  var hash = new HMACSHA256(keyBytes);
                  var computedHash = hash.ComputeHash(requestData);
                  var computedHashString = Convert.ToBase64String(computedHash);
          
                  if (extractedSignature != computedHashString)
                  {
                      this._log.LogError("Sha Auth failed. Signature is not valid");
          
                      context.Result = new ContentResult()
                      {
                          StatusCode = 401,
                          Content = "Signature is not valid",
                      };
                  }
          
                  this._log.LogInformation("Successfully passed Sha Auth.");
              }
          }
          

          你还需要在你的 startup.cs 中添加这个:

          // https://stackoverflow.com/questions/47735133/asp-net-core-synchronous-operations-are-disallowed-call-writeasync-or-set-all
          // Needed for ShaAuth
          services.Configure<KestrelServerOptions>(options =>
            {
                 options.AllowSynchronousIO = true;
             });
          

          这是我的控制器:

              [AllowAnonymous]
              [HttpPost]
              [ServiceFilter(typeof(ShaKeyAttribute))]
              public async Task<IActionResult> Create([FromBody] WooCommerceRequest request)
              {
                  this._log.LogInformation("Executing Create of Subscription controller...");
          
                  var response = await this._mediator.Send(new AddSubscriptionCommand() { OrderId = request.OrderId });
          
                  this._log.LogInformation("Finished executing Create of Subscription controller.");
          
                  return this.Ok();
              }
          

          希望这对某人有所帮助。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2016-03-16
            • 2017-03-24
            • 2020-07-17
            • 2021-12-25
            • 2022-06-28
            • 1970-01-01
            • 2017-06-22
            • 1970-01-01
            相关资源
            最近更新 更多