POST, PUT, DELETE: Building Custom Requests from Zero
If you only test GET requests, you are only testing half the application.Press enter or click to vie 2026-5-15 05:33:59 Author: infosecwriteups.com(查看原文) 阅读量:9 收藏

If you only test GET requests, you are only testing half the application.

Roshan Rajbanshi

Press enter or click to view image in full size

Series: curl — The Request Engine You Never Learned Properly Article: 5 of 16

Web applications do not just respond to GET requests. They expose POST endpoints for form submissions and logins, PUT and PATCH endpoints for updating resources, DELETE endpoints for removing them, and OPTIONS endpoints that tell you what the server allows before you choose an attack vector.

Most curl beginners stay on GET. That means they are only testing one dimension of an application’s attack surface.

This article covers the full request construction toolkit: every HTTP method a pentester needs, the Content-Type problem that silently breaks many beginner API tests, and file upload requests built from the ground up.

HTTP Methods as an Attack Surface Map

Every HTTP method represents a different interaction with the server — and a different potential attack surface.

GET — Retrieve a resource. Parameters travel in the URL. Most servers log GET requests, and browsers and proxies cache them. Never use GET to send sensitive data — it ends up in logs.

POST — Submit data to be processed. The body carries the payload. It is the method for login forms, API calls that create resources, and file uploads — the most common method you will use in attack workflows.

PUT — Replace a resource entirely. Less common on web apps but common on REST APIs. A PUT endpoint that accepts arbitrary content can indicate write-like behavior worth testing. Test: Can you PUT to a path and then GET it back? Many APIs require authentication for PUT or disable it entirely.

PATCH — Partially update a resource. Similar attack surface to PUT, but for partial modifications. Some APIs expose PATCH but not PUT, and most require authentication for either.

DELETE — Remove a resource. A DELETE endpoint without proper authorization controls is a finding on its own. Can you delete resources belonging to other users? Can you delete admin resources as a regular user?

OPTIONS — Ask the server what methods it accepts on a given endpoint. Often returns a Allow header listing permitted methods, though not every server responds meaningfully — some ignore OPTIONS entirely or return it only on 405 responses. Run OPTIONS as a starting point, not a definitive map.

HEAD — Same as GET, but returns only headers. Covered in Article 4. Useful for checking whether a resource exists without downloading it.

The OPTIONS Recon Step

Before choosing how to attack an endpoint, ask the server what it accepts:

curl -X OPTIONS -i http://localhost:8080
HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.8.10
Date: Fri, 24 Apr 2026 11:17:12 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 426
X-Lab-Server: curl-series-echo-v1
==================================================
curl Lab Echo Server
==================================================
METHOD : OPTIONS
PATH : /
FULL URL : /
--- REQUEST HEADERS ---
Host: localhost:8080
User-Agent: curl/7.68.0
Accept: */*
--- QUERY STRING PARAMS ---
(none)
--- RAW BODY ---
(empty)
--- PARSED BODY PARAMS ---
(none)
==================================================

The lab echo server (output above) confirms the method arrived as OPTIONS. On a real application, the response headers may include an Allow header. Note that Allow shows which methods the server claims to support — not which are exploitable, unauthenticated, or reachable without specific roles. It is a starting map, not a guarantee.

On a real REST API, the response headers might contain:

Allow: GET, POST, PUT, DELETE, OPTIONS

This tells you which methods are worth testing on this endpoint. A response of Allow: GET only means your PUT and DELETE tests will likely fail before reaching the application logic — save the time.

Some servers also return a Public header listing globally available methods, and some add access control headers that reveal whether the endpoint requires authentication for certain methods. Read everything the OPTIONS response gives you.

One nuance worth knowing: on some servers, the Allow header only appears in 405 Method Not Allowed responses, not in OPTIONS responses. If OPTIONS returns nothing useful, try sending an unsupported method and read the 405 response instead.

Building POST Requests

The basic form POST:

curl -d "username=admin&password=password123" http://localhost:8080/login
==================================================
curl Lab Echo Server
==================================================
METHOD       : POST
PATH : /login
FULL URL : /login
--- REQUEST HEADERS ---
Host: localhost:8080
User-Agent: curl/7.68.0
Accept: */*
Content-Length: 35
Content-Type: application/x-www-form-urlencoded
--- RAW BODY ---
username=admin&password=password123
--- PARSED BODY PARAMS ---
username = admin
password = password123
Total params received: 2
==================================================

Press enter or click to view image in full size

The server received two cleanly parsed parameters. curl is set Content-Type: application/x-www-form-urlencoded automatically and calculated Content-Length: 35 without being asked. The PARSED BODY PARAMS section confirms that both values arrived correctly.

Remember from Article 3: -d sends data as-is. If your password contains & or =, They will break the parameter structure. For passwords with special characters:

curl --data-urlencode "username=admin" \
--data-urlencode "password=p@ss&word=1" \
http://target.com/login

The Content-Type Problem

This is the single most common silent failure in beginner API testing, and it deserves careful attention.

When you send data to a server, the server needs to know how to parse it. The Content-Type header tells the server what format the body is in. Send the wrong Content-Type — or omit it — and the server may reject your data, misparse it, or return an error that has nothing to do with your actual payload.

Case 1: Sending JSON without the JSON Content-Type

# Wrong — sends JSON body but tells server it's form data
curl -d '{"username":"admin","password":"test"}' http://localhost:8080/api/login
--- REQUEST HEADERS ---
Content-Type: application/x-www-form-urlencoded
--- RAW BODY ---
{"username":"admin","password":"test"}
--- PARSED BODY PARAMS ---
{"username":"admin","password":"test"} =
Total params received: 1

Press enter or click to view image in full size

The PARSED BODY PARAMS section is clear evidence of the problem. The lab server treated the entire JSON string as a single key with an empty value — {"username":"admin","password":"test"} =. That is not a username and password. That is a malformed key that the application cannot use. curl's default Content-Type for -d is application/x-www-form-urlencoded. The beginner assumes the API is broken. The API works fine. The Content-Type was wrong.

Note: the output above is from the curl lab echo server — it shows how the server parsed the incoming data. A real API server would typically return a 400 error or an authentication failure instead of echoing the body back.

# Correct:
curl -H "Content-Type: application/json" \
-d '{"username":"admin","password":"test"}' \
http://localhost:8080/api/login
--- REQUEST HEADERS ---
Content-Type: application/json
--- RAW BODY ---
{"username":"admin","password":"test"}
--- PARSED BODY PARAMS ---
(none)

Same RAW BODY. Completely different Content-Type header. With the correct header, the server identifies it as JSON and does not attempt to parse it as form data — PARSED BODY PARAMS shows (none) because the lab server's form parser correctly skips JSON bodies. A real JSON API would route this to its JSON handler and extract the fields cleanly.

Case 2: Sending form data with JSON Content-Type

The reverse problem also occurs. If an endpoint expects application/x-www-form-urlencoded , and you send Content-Type: application/json With form-encoded data, the server's JSON parser will fail to parse the body.

The three Content-Types you need to know:

Content-Type                          Used for                curl flag
----------------------------- ---------------------- ----------------------------------
application/x-www-form-urlencoded HTML form submissions Default with -d
application/json REST API calls -H "Content-Type: application/json"
multipart/form-data File uploads Automatic with -F

Check the API documentation or intercept a legitimate request to confirm which Content-Type the endpoint expects. When in doubt, try both form-encoded and JSON — different Content-Types sometimes reach different code paths in the same application.

JSON API Testing: The Complete Pattern

A complete JSON API request:

curl -s \
-X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"username":"admin","password":"password"}' \
http://localhost:8080/api/v1/login
==================================================
curl Lab Echo Server
==================================================
METHOD       : POST
PATH : /api/v1/login
FULL URL : /api/v1/login
--- REQUEST HEADERS ---
Host: localhost:8080
User-Agent: curl/7.68.0
Content-Type: application/json
Accept: application/json
Content-Length: 42
--- RAW BODY ---
{"username":"admin","password":"password"}
--- PARSED BODY PARAMS ---
(none)
==================================================

Breaking down the flags:

  • -s — silent, no progress meter
  • -X POST — explicit method (redundant with -d but clear)
  • -H "Content-Type: application/json" — tells the server what you are sending
  • -H "Accept: application/json" — tells the server what format you want back
  • -d — the JSON body

The Accept header is worth including. Many APIs use it for content negotiation — they return JSON when asked and HTML by default. Some APIs ignore it entirely, but including it costs nothing and prevents the case where you receive an unhelpful HTML error page when the API actually supports JSON responses.

Get Roshan Rajbanshi’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

Sending nested JSON:

curl -s \
-X POST \
-H "Content-Type: application/json" \
-d '{"user":{"name":"admin","role":"user"},"token":"abc123"}' \
http://target.com/api/profile

Reading a JSON API endpoint:

curl -s \
-H "Accept: application/json" \
-H "Authorization: Bearer eyJ..." \
http://target.com/api/v1/users | python3 -m json.tool

Piping through python3 -m json.tool pretty-prints JSON responses. Article 13 covers jq for more powerful JSON processing.

PUT and PATCH

Testing a PUT endpoint:

curl -X PUT \
-H "Content-Type: application/json" \
-d '{"name":"updated","email":"[email protected]"}' \
http://localhost:8080/api/users/5
==================================================
curl Lab Echo Server
==================================================
METHOD       : PUT
PATH : /api/users/5
FULL URL : /api/users/5
--- REQUEST HEADERS ---
Host: localhost:8080
User-Agent: curl/7.68.0
Accept: */*
Content-Type: application/json
Content-Length: 42
--- RAW BODY ---
{"name":"updated","email":"[email protected]"}
--- PARSED BODY PARAMS ---
(none)
==================================================

Note the path — /api/users/5. The 5 is a user ID. In a real application, this is where IDOR testing begins: can you PUT to /api/users/6 and modify another user's record?

Testing a PATCH endpoint:

curl -X PATCH \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]"}' \
http://target.com/api/users/5

Testing attack scenarios:

  • Can you PUT to an endpoint you do not own? (IDOR — Insecure Direct Object Reference)
  • Can you PUT content that the server will serve back? (Stored XSS via PUT)
  • Can you PUT to paths outside the API structure? (Path traversal in PUT)

DELETE

curl -X DELETE http://localhost:8080/api/users/5
==================================================
curl Lab Echo Server
==================================================
METHOD       : DELETE
PATH : /api/users/5
FULL URL : /api/users/5
--- REQUEST HEADERS ---
Host: localhost:8080
User-Agent: curl/7.68.0
Accept: */*
--- RAW BODY ---
(empty)
--- PARSED BODY PARAMS ---
(none)
==================================================

DELETE often has no body — HTTP does not strictly forbid one, but most applications ignore it. The entire instruction is in the method and the path. On a real target, the authorization check is on the server — if it is missing or inconsistent, a DELETE to another user’s resource ID succeeds.

DELETE endpoints are often undertested. Authorization checks on DELETE are sometimes implemented inconsistently — a developer who correctly protected the GET endpoint forgot the DELETE. Test:

# As an authenticated user, can you delete another user's resource?
curl -X DELETE \
-H "Authorization: Bearer <your_token>" \
http://target.com/api/users/<other_users_id>

A 200 or 204 on that request is a finding.

File Upload Requests: Multipart Form Data

File upload endpoints are one of the most productive attack surfaces in web applications. Unrestricted file upload can lead to remote code execution. Curl handles multipart uploads with the -F flag.

Basic file upload:

curl -F "file=@/path/to/file.txt" http://target.com/upload

The @ prefix tells curl to read the file from disk. The field name (file) must match what the server expects — check the HTML form or API documentation.

The anatomy of what -F sends:

echo "test file content" > /tmp/test.txt
curl -v -F "file=@/tmp/test.txt" http://localhost:8080 2>&1 | head -60
> POST / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
> Content-Length: 204
> Content-Type: multipart/form-data; boundary=------------------------fdd2ae0863b865ba
--- RAW BODY ---
--------------------------fdd2ae0863b865ba
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
test file content --------------------------fdd2ae0863b865ba--

Press enter or click to view image in full size

Press enter or click to view image in full size

Read the RAW BODY carefully. This is the actual wire format of a multipart request — the structure that -F builds automatically. Three things to note. First, the boundary string (fdd2ae0863b865ba) — curl generates this randomly and uses it to separate parts. Second, each part has its own Content-Disposition header containing the field name and filename. Third, each part has its own Content-Type — here text/plain because curl detected the file as plain text. When you use ;type=image/jpeg To override the Content-Type, you are changing this per-part header, not the outer multipart header. That is the mechanism behind upload Content-Type bypass.

Adding additional form fields:

curl -F "[email protected]" \
-F "description=profile picture" \
-F "type=image" \
http://target.com/upload

Overriding the Content-Type of the uploaded file:

curl -F "[email protected];type=image/jpeg" http://target.com/upload

The ;type=image/jpeg suffix overrides the Content-Type that curl assigns to the file part — specifically the per-part header inside the multipart body, not the file's actual contents. If the server validates uploaded files based solely on the Content-Type header, this may bypass that check. Many modern systems go further and inspect file contents, magic bytes, or extensions server-side, so this is not a universal bypass. Article 10 covers the full methodology: double extensions, MIME type mismatches, and Content-Type spoofing in depth.

Real Walkthrough: Login, Cookie Jar, Authenticated Session

The following walkthrough uses the TryHackMe Advent of Cyber 2025 curl room — a purpose-built target with a login form and cookie-based authentication.

Step 1 — Identify the login endpoint:

curl -sI http://10.48.143.207/post.php
HTTP/1.1 200 OK
Date: Fri, 24 Apr 2026 11:43:51 GMT
Server: Apache/2.4.52 (Ubuntu)
Content-Type: text/html; charset=UTF-8

Press enter or click to view image in full size

Server: Apache/2.4.52 (Ubuntu) — already a data point. Content-Type: text/html confirms this endpoint returns HTML, not JSON. The login likely expects standard form data.

Step 2 — Attempt form login and save cookie:

curl -s \
-c cookies.txt \
-d "username=admin&password=admin" \
http://10.48.143.207/cookie.php
Login successful. Cookie set.

The response message above is from the TryHackMe lab server — not curl output. curl printed whatever the server returned. The important thing is that -c cookies.txt wrote the session cookie to disk silently in the background. You did not have to manually copy a token — curl handled the entire cookie jar automatically.

Note: cookie persistence depends on what the server sets — the domain, path, and expiration in the Set-Cookie header all affect whether -c captures the cookie correctly.

Step 3 — Verify session with saved cookie:

curl -s \
-b cookies.txt \
http://10.48.143.207/cookie.php
Welcome back, admin!

Again, the server produced that message — curl sent the request and printed whatever came back. What matters is that -b cookies.txt Read the cookie from disk and include it automatically. The server recognized the session and returned the authenticated response. This pattern — login with -c, reuse session with -b — is the foundation of every multi-step attack workflow in this series. Authenticated scanning, session hijacking tests, and CSRF probing — all of them start here.

Quick Reference — Article 5

# OPTIONS recon — what does this endpoint accept?
curl -X OPTIONS -i http://target.com/api/endpoint
# POST — form data
curl -d "username=admin&password=test" http://target.com/login
# POST — JSON (always set Content-Type)
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"username":"admin","password":"test"}' \
http://target.com/api/login
# PUT — replace resource
curl -X PUT \
-H "Content-Type: application/json" \
-d '{"name":"updated","email":"[email protected]"}' \
http://target.com/api/users/5
# PATCH — partial update
curl -X PATCH \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]"}' \
http://target.com/api/users/5
# DELETE — remove resource
curl -X DELETE http://target.com/api/users/5
# File upload — multipart
curl -F "file=@/path/to/file.php" http://target.com/upload
# File upload — override Content-Type (bypass)
curl -F "[email protected];type=image/jpeg" http://target.com/upload
# Cookie jar — login and save session
curl -s -c cookies.txt -d "username=admin&password=admin" http://target.com/login
# Cookie jar — reuse saved session
curl -s -b cookies.txt http://target.com/dashboard
# Pretty-print JSON response
curl -s -H "Accept: application/json" http://target.com/api/data | python3 -m json.tool
CONTENT-TYPE DECISION
----------------------
Sending form data? Default -d (application/x-www-form-urlencoded)
Sending JSON to an API? -H "Content-Type: application/json" + -d '{"key":"val"}'
Uploading a file? -F (multipart/form-data — set automatically)
Not sure which? Try form first, then JSON — they reach different code paths

The habit this article builds: before testing any web application function, identify the method, confirm the Content-Type, and run OPTIONS. The attack surface is in the methods the server exposes — you need to see all of them before deciding where to focus.

Next: Article 6A — Auth Mastery Part 1: Credential Types curl Handles


文章来源: https://infosecwriteups.com/post-put-delete-building-custom-requests-from-zero-abd73dd88d59?source=rss----7b722bfd1b8d---4
如有侵权请联系:admin#unsafe.sh