Header values containing a double quote are silently dropped (breaks quoted ETags in If-Match)
- Ngôn ngữ chính
- Emacs Lisp
- Star
- 236
- Fork
- 19
- Chỉ số merge pull request
- Không có pull request nào được merge trong 30 ngày
Mô tả
# plz drops (silently) any request header whose value contains a double quote
## Environment
- plz 0.9.1
- GNU Emacs 30.2 (the only version tested). The defect is in plz's curl-`--config`
serialization + curl's config-file parsing — neither is Emacs-version-dependent —
so it is expected on any Emacs running plz 0.9.1, but I have only confirmed 30.2.
- curl (any recent version; the behavior is curl's stable `--config` quoting)
## Summary
`plz` passes request headers to curl through a `--config` file (`curl --config -`),
writing each one as a line of the form:
```
--header "NAME: VALUE"
```
The value is wrapped in double quotes but is **not escaped**. curl's config-file
parser treats `"` as the string delimiter (with `\"` as the escape), so a header
**value** that itself contains a `"` closes the quoted argument early. The header
is truncated to an empty value, and curl then omits empty-valued headers — so the
header never reaches the server, with no error or warning.
This bites any HTTP header whose value is legitimately quoted. The most common
case is an **`ETag`**: strong ETags are quoted by RFC 9110, and echoing one back
as `If-Match` / `If-None-Match` is the standard conditional-request pattern. Under
plz, those preconditions silently never take effect.
## Reproduction (self-contained; no external server)
```elisp
;; emacs --batch -l plz -l plz-if-match-drop.el
(require 'plz)
(let* ((port 18093)
(captured "")
(server (make-network-process
:name "cap" :server t :host "127.0.0.1" :service port :family 'ipv4
:filter (lambda (proc str)
(setq captured (concat captured str))
(ignore-errors
(process-send-string
proc "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"))))))
(unwind-protect
(progn
(ignore-errors
(plz 'put (format "http://127.0.0.1:%d/x" port)
:headers '(("If-Match" . "\"abc123\"") ; quoted value (a strong ETag)
("X-Control" . "plainvalue")) ; control: no quote
:body "b" :connect-timeout 2 :timeout 3 :as 'response))
(sleep-for 0.3)
(let ((case-fold-search t))
(princ (format "If-Match on the wire: %s\n"
(if (string-match-p "^if-match:" captured) "YES" "NO (dropped)")))
(princ (format "X-Control on the wire: %s\n"
(if (string-match-p "^x-control:" captured) "YES" "NO")))))
(delete-process server)))
```
### Expected
Both headers are sent; `If-Match: "abc123"` reaches the server.
### Actual
```
If-Match on the wire: NO (dropped)
X-Control on the wire: YES
```
The quoted-value header is dropped; the plain-value control header is sent.
## Root cause
In `plz` (plz.el 0.9.1), the per-header config args are built and then serialized
into the config string without escaping the value:
```elisp
;; header args:
(curl-config-header-args (cl-loop for (key . value) in headers
collect (cons "--header" (format "%s: %s" key value))))
;; ...
;; config serialization (the bug — value embedded in "%s" unescaped):
(curl-config (cl-loop for (key . value) in curl-config-args
concat (format "%s \"%s\"\n" key value)))
```
For `If-Match: "abc123"` this emits the config line:
```
--header "If-Match: "abc123""
```
which curl reads as the header `If-Match:` (empty — parsing stops at the embedded
`"`) and omits.
curl-level demonstration (feeding each line to `curl --config - --trace-ascii`,
observing the wire):
```
[plz-0.9.1] --header "If-Match: "abc123"" -> If-Match: (none — dropped)
[with the fix] --header "If-Match: \"abc123\"" -> If-Match: "abc123"
```
## Proposed fix
Backslash-escape `\` and `"` in each config value before wrapping it, so curl's
config parser reconstructs the literal value:
```diff
(curl-config (cl-loop for (key . value) in curl-config-args
- concat (format "%s \"%s\"\n" key value)))
+ concat (format "%s \"%s\"\n" key
+ (replace-regexp-in-string
+ "[\\\"]" "\\\\\\&" value))))
```
`replace-regexp-in-string` scans once and does not re-scan inserted text, so `\`
becomes `\\` and `"` becomes `\"` — each escaped exactly once, no double-escaping.
Values without either character are unchanged, so existing requests are unaffected.
Verified: with the patch applied to `plz.el` (loaded via `emacs -Q -l plz.el`), the
reproduction above prints `If-Match on the wire: YES` (and `X-Control … YES`). The
patched file byte-compiles cleanly with `byte-compile-error-on-warn`.
## Impact / workaround
Any conditional request (`If-Match`/`If-None-Match` with a strong ETag), or any
header whose value contains `"`, is silently not sent. A caller-side workaround is
to pre-escape `"`/`\` in header values, but that couples callers to plz's internal
config-file quoting; the fix belongs in plz.
Hướng dẫn đóng góp
Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này
Đánh giá
Issue này chưa được đánh giá.