Skip to content

fix(decode): handle unquoted Content-Disposition name parameter#45

Merged
ms2008 merged 1 commit into
masterfrom
fix/unquoted
Mar 31, 2026
Merged

fix(decode): handle unquoted Content-Disposition name parameter#45
ms2008 merged 1 commit into
masterfrom
fix/unquoted

Conversation

@cshuaimin

@cshuaimin cshuaimin commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

The name extraction unconditionally stripped the first and last characters assuming surrounding quotes. When the name value was unquoted (e.g. name=login_id), this truncated the field name (login_id -> ogin_i), making it inaccessible by its real key.

In the lua pattern I changed [^=] to [^"] and I think it should be OK.

Fix FTI-7402

The name extraction unconditionally stripped the first and last
characters assuming surrounding quotes. When the name value was
unquoted (e.g. name=login_id), this truncated the field name
(login_id -> ogin_i), making it inaccessible by its real key.
@cshuaimin cshuaimin requested a review from Copilot March 30, 2026 08:18
@github-actions

Copy link
Copy Markdown

Luacheck Report

2 tests   2 ✅  0s ⏱️
1 suites  0 💤
1 files    0 ❌

Results for commit ade7587.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes multipart Content-Disposition parsing so name= values are no longer incorrectly truncated when the parameter is unquoted, restoring access to fields by their real keys (FTI-7402).

Changes:

  • Update decode() to extract the name parameter without assuming surrounding quotes.
  • Add a regression test covering unquoted name=login_id / name=api_key multipart parts.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/multipart.lua Adjusts Content-Disposition name extraction pattern to handle unquoted values.
spec/multipart_spec.lua Adds test ensuring unquoted field names are decoded and retrievable correctly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/multipart.lua
if pos then
local current_value = v:match("^%s*([^=]*)", pos):gsub("%s*$", "")
part_name = sub(current_value, 2, #current_value - 1)
part_name = v:match('^%s*"?([^"]*)"?', pos):gsub("%s*$", "")

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new name extraction only strips optional double quotes. Previously, single-quoted values like name='login_id' would be normalized to login_id, but with this change they will be returned including the single quotes (a backwards-incompatible behavior change). Consider supporting both quote styles (similar to boundary parsing) or explicitly stripping matching surrounding quotes for either ' or ", and add a regression test if this input is expected.

Suggested change
part_name = v:match('^%s*"?([^"]*)"?', pos):gsub("%s*$", "")
local raw_name = v:match("^%s*(.-)%s*$", pos)
if raw_name then
local first = sub(raw_name, 1, 1)
local last = sub(raw_name, -1)
if #raw_name >= 2 and first == last and (first == '"' or first == "'") then
part_name = sub(raw_name, 2, -2)
else
part_name = raw_name
end
part_name = part_name:gsub("%s*$", "")
end

Copilot uses AI. Check for mistakes.

@cshuaimin cshuaimin Mar 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP generally uses double quotes. For single quotes the following tests shows that Python (httpie)'s and Go (mccutchen/go-httpbin)'s implementation escapes double quotes but leave single quotes unchanged, making single quotes as part of the key name as-is. I don't have a strong opinion though.

$ http :5000/post foo=1 \"bar\"=2 --multipart -v
POST /post HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 214
Content-Type: multipart/form-data; boundary=0ae2a49dc7bb496eacb1ad2a56f78eae
Host: localhost:5000
User-Agent: HTTPie/3.2.4

--0ae2a49dc7bb496eacb1ad2a56f78eae
Content-Disposition: form-data; name="foo"

1
--0ae2a49dc7bb496eacb1ad2a56f78eae
Content-Disposition: form-data; name="%22bar%22"

2
--0ae2a49dc7bb496eacb1ad2a56f78eae--


HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Content-Length: 857
Content-Type: application/json; charset=utf-8
Date: Mon, 30 Mar 2026 08:34:55 GMT

{
    "args": {},
    "data": "--0ae2a49dc7bb496eacb1ad2a56f78eae\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n1\r\n--0ae2a49dc7bb496eacb1ad2a56f78eae\r\nContent-Disposition: form-data; name=\"%22bar%22\"\r\n\r\n2\r\n--0ae2a49dc7bb496eacb1ad2a56f78eae--\r\n",
    "files": {},
    "form": {
        "%22bar%22": [
            "2"
        ],
        "foo": [
            "1"
        ]
    },
    "headers": {
        "Accept": [
            "*/*"
        ],
        "Accept-Encoding": [
            "gzip, deflate"
        ],
        "Connection": [
            "keep-alive"
        ],
        "Content-Length": [
            "214"
        ],
        "Content-Type": [
            "multipart/form-data; boundary=0ae2a49dc7bb496eacb1ad2a56f78eae"
        ],
        "Host": [
            "localhost:5000"
        ],
        "User-Agent": [
            "HTTPie/3.2.4"
        ]
    },
    "json": null,
    "method": "POST",
    "origin": "192.168.10.49",
    "url": "http://localhost:5000/post"
}
$ http :5000/post foo=1 \'bar\'=2 --multipart -v
POST /post HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 210
Content-Type: multipart/form-data; boundary=691735ed9a344534a3664dff07a9c3f9
Host: localhost:5000
User-Agent: HTTPie/3.2.4

--691735ed9a344534a3664dff07a9c3f9
Content-Disposition: form-data; name="foo"

1
--691735ed9a344534a3664dff07a9c3f9
Content-Disposition: form-data; name="'bar'"

2
--691735ed9a344534a3664dff07a9c3f9--


HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Content-Length: 849
Content-Type: application/json; charset=utf-8
Date: Mon, 30 Mar 2026 08:34:39 GMT

{
    "args": {},
    "data": "--691735ed9a344534a3664dff07a9c3f9\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n1\r\n--691735ed9a344534a3664dff07a9c3f9\r\nContent-Disposition: form-data; name=\"'bar'\"\r\n\r\n2\r\n--691735ed9a344534a3664dff07a9c3f9--\r\n",
    "files": {},
    "form": {
        "'bar'": [
            "2"
        ],
        "foo": [
            "1"
        ]
    },
    "headers": {
        "Accept": [
            "*/*"
        ],
        "Accept-Encoding": [
            "gzip, deflate"
        ],
        "Connection": [
            "keep-alive"
        ],
        "Content-Length": [
            "210"
        ],
        "Content-Type": [
            "multipart/form-data; boundary=691735ed9a344534a3664dff07a9c3f9"
        ],
        "Host": [
            "localhost:5000"
        ],
        "User-Agent": [
            "HTTPie/3.2.4"
        ]
    },
    "json": null,
    "method": "POST",
    "origin": "192.168.10.49",
    "url": "http://localhost:5000/post"
}

@houmkh houmkh left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A string of text is parsed as a single value if it is quoted using double-quote marks.

https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
From this document, it seems that only double quotes need to be handled.

@ms2008 ms2008 merged commit d32d53f into master Mar 31, 2026
17 checks passed
@ms2008 ms2008 deleted the fix/unquoted branch March 31, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants