8 min read

Generic HTTP tools erase the permission boundary

Generic HTTP tools hide many distinct powers behind one call. Learn to authorize credentials, destinations, methods, and request details separately.

Generic HTTP tools erase the permission boundary

A generic HTTP call looks like one tool to an agent host. Operationally, it can be thousands of capabilities. Change the URL, credential, method, headers, or body and the same function can read a status page, export customer records, rotate a signing secret, or delete a production resource.

That mismatch makes tool-level permission a weak security boundary. An approval that says an agent may use http_request does not tell you what the agent may do. The useful decision sits one layer lower, where the system can bind a specific credential to a resolved destination, an allowed method, and constrained request details.

The distinction matters even when the agent is behaving well. Prompts contain mistakes, retrieved text can contain hostile instructions, and a broad tool can turn a small planning error into a real API action. You do not fix that by giving the tool a friendlier name. You fix it by authorizing the action that will cross the network.

One call tool contains many capabilities

A tool is only a capability boundary when its inputs cannot radically change its authority. A weather lookup with a fixed upstream, fixed GET method, and fixed response shape is close to one capability. A function that accepts any URL, verb, headers, and body is a programmable network client.

Consider a schema with five ordinary fields: url, method, headers, body, and credential_name. The agent host may present that as one item in a tool list. Yet GET /projects/42 with a read token and DELETE /projects/42 with an administrator token do not deserve the same decision. Nor does a request to a public API deserve the same decision as a request that resolves to a service on a private network.

Model Context Protocol tool annotations do not repair this collapse. The MCP tools specification describes annotations such as read-only and destructive behavior as hints, and it tells clients to treat them as untrusted unless they come from trusted servers. A generic caller also cannot assign one truthful value to readOnlyHint: its behavior depends on arguments that have not arrived yet.

This is the distinction teams routinely blur: tool selection answers which implementation will run, while action authorization answers which external effect may occur. If the host approves only the implementation name, every materially different request inherits that approval. A neat tool picker can therefore sit in front of an almost unrestricted egress channel.

Splitting every API operation into a separate tool can improve descriptions and planning, but it is not a complete security answer. Generated tools still need an enforcement point, credentials still carry their own authority, and redirects can move a seemingly narrow operation somewhere else. Use specific tools for usability. Enforce permissions on the final request.

Permission needs four coordinates

A defensible HTTP decision has four coordinates: the credential, the resolved destination, the method, and the request details. Remove any one and a request can keep the approved appearance while changing its effect.

The credential identifies the authority being exercised. A deployment token and a billing token may target the same host and path but expose unrelated powers. The destination identifies where that authority may be presented. The method expresses broad HTTP intent. The remaining request details, including path, query, selected headers, content type, and body fields, determine the actual operation.

A compact authorization record can look like this:

credential: issue-tracker-read
origin: https://api.example.test:443
path_prefix: /v2/issues/
methods:
  - GET
redirects: deny
headers:
  allow:
    - Accept
query:
  deny:
    - include_deleted
body: forbidden

This fragment prevents several failures at once. The agent cannot swap in a more powerful credential, send the credential to another origin, turn the read into a POST, follow a redirect, add an authorization-like header, request deleted records through that query switch, or smuggle a body into an operation expected to be read-only.

The policy object is not a universal template. Some APIs need exact paths, some need a tenant identifier in the path, and some expose one action through a POST body. The point is the shape of the decision: evaluate all four coordinates together after parsing and normalization, then inject the credential only if the request passes.

Do not let the agent provide the actual secret through headers. Let it refer to a credential by an opaque name, then have a trusted executor add the bearer token, basic credentials, or custom header after authorization. Otherwise the agent can copy a secret into another field, a log, or a second destination, and your careful destination rule becomes theater.

Credentials define authority, not convenience

Credentials should be separate grants with the narrowest authority the upstream API supports. A generic HTTP tool becomes much less dangerous when the executor chooses among named, scoped credentials rather than holding one organization-wide token.

OAuth scopes help, but scope and audience answer different questions. A scope may say that a token can read contacts. The audience says which resource server should accept it. RFC 8707 defines the OAuth resource parameter so a client can request a token for a specific protected resource, and it recommends that authorization servers audience-restrict the issued token. Its security argument is practical: a token presented to one resource should not become usable at another.

The current MCP authorization specification applies the same separation. It requires MCP servers to accept tokens intended for themselves and forbids passing an inbound MCP token through to an upstream API. When an MCP server calls another API, it acts as a separate OAuth client and uses a separate upstream token. That is a clean boundary, and a generic HTTP executor should preserve it even when no OAuth flow is visible to the agent.

API keys often provide weaker native controls. Treat each one as a distinct authority anyway. Record which origins may receive it, which header form the executor will inject, when it expires, and who owns rotation. Never interpret a vault entry called staging as harmless merely because of its name. Verify its upstream privileges.

Credential choice also belongs in the approval display. A prompt that says POST api.example.test leaves out whether the call uses a sandbox key or an account-owner token. Show a human-readable credential label and its intended account or tenant, but never show secret material. If the executor cannot identify that context, the grant is too ambiguous for quiet reuse.

Keeping secrets outside the model reduces accidental disclosure, but secrecy alone does not constrain use. An agent can misuse a secret without ever seeing its bytes if a broad executor will attach it to arbitrary requests. Non-disclosure and least authority solve different problems. You need both.

Destination means the resolved endpoint

Checking a URL string once is not destination control. The executor must parse the URL, normalize it, resolve the host, enforce network rules, and repeat the decision for every redirect before attaching a credential.

Start with an exact scheme, host, and effective port. https://api.example.test and https://api.example.test:8443 are different origins. Reject user information in URLs, ambiguous encodings, unsupported schemes, and hostnames that only look like approved suffixes. A suffix check that accepts api.example.test.attacker.invalid is not an allowlist.

Then resolve DNS and inspect every returned address. A public-looking hostname can resolve to loopback, link-local, a private range, or cloud instance metadata. Resolution can also change between validation and connection. The component that validates should control the connection and verify the address it actually uses, rather than handing the URL to a second client that resolves it again.

OWASP's Server Side Request Forgery Prevention Cheat Sheet recommends allowlisting known trusted destinations when the application can identify them. It also advises disabling automatic redirect following because a redirect can bypass input validation. That advice fits agent tools especially well: the URL is often model-supplied, and the request may carry a credential that the first destination was allowed to receive.

Redirects require a fresh authorization decision. RFC 9110 says automatic redirects need care for unsafe methods and recommends removing resource-specific fields such as Authorization and Cookie when following them. A secure executor can be simpler: deny redirects by default for authenticated calls, or surface the new destination as a new action and inject credentials again only after it passes policy.

Path controls still matter within an approved origin. Multi-tenant gateways, shared SaaS hosts, and administrative routes can live behind one hostname. RFC 8707 notes that a tenant-identifying path may need to be part of the resource identifier in multi-tenant systems. An origin allowlist without a path or tenant constraint can therefore be much broader than its reviewer expects.

HTTP methods are signals, not verdicts

Require consent for every use
Mark a sensitive API key for one-click or Touch ID approval on each request.

Method restrictions remove a large class of mistakes, but the method name cannot prove that a request is harmless. RFC 9110 defines GET, HEAD, OPTIONS, and TRACE as safe because their specified semantics are essentially read-only. It defines PUT, DELETE, and safe methods as idempotent, which means repeating the intended operation has the same intended effect as making it once.

Safe and idempotent are not synonyms. DELETE can be idempotent while still destroying a resource. POST is generally neither safe nor idempotent, but an API may use POST for a read-only search because the query is too large for a URL. Approval systems that reduce risk to GET versus POST will misclassify both cases.

Worse, real APIs sometimes violate method semantics. RFC 9110 explicitly warns about resources that place actions such as deletion in a GET query and says the resource owner must disable unsafe behavior through safe methods. Your executor cannot assume every upstream follows that rule. If GET /jobs?id=7&action=cancel changes state, allowing all GET requests did not create a read-only grant.

Use the method as one input. Pair it with a path and, where necessary, operation-specific request constraints. For a well-described API, an OpenAPI operation can supply a useful map: the OpenAPI Specification lets operations declare security requirements, and OAuth entries list required scopes. Import that information as configuration, then verify it against what the upstream actually issues and accepts. A description file is not an enforcement point by itself.

Retry behavior belongs in this decision too. A network timeout after a POST does not tell the caller whether the server applied the action. Do not automatically retry an unsafe, non-idempotent request unless the API supplies an idempotency mechanism or the caller can establish that the first request was not applied. By contrast, idempotence can make a retry operationally safer, but it does not make the original action authorized.

Request details decide the real effect

Two requests with the same credential, origin, path, and method can still do opposite things. The body, query, and selected headers often contain the operation that matters.

A billing endpoint might accept POST /v1/subscriptions/update for both reducing a seat count and moving an account to an expensive plan. A repository endpoint might use one mutation route with an operation field for archive, transfer, or delete. A search endpoint might expose hidden or deleted records when include_deleted=true appears. Granting the route without constraining those fields grants every mode the route implements.

Headers need equal suspicion. The executor should own Authorization, Proxy-Authorization, Host, and any custom credential header. It should normally reject attempts by the agent to set them. Headers that select an account, impersonate a user, override a method, request asynchronous execution, or carry conditional write controls can change authority or effect. Forwarding arbitrary headers is another generic tool hiding inside the first one.

Content type controls parsing. If policy inspects JSON but the client may send form data, multipart content, or compressed bytes, an agent can move the sensitive field outside the parser. Enforce the declared type, set size limits before buffering, reject duplicate or ambiguous fields, and authorize the same byte representation that the executor sends. Validation of one object followed by reserialization of another invites gaps.

Response handling also belongs to the boundary, although it is not a permission coordinate for the outgoing effect. Limit response size, classify content types, and treat returned instructions as untrusted data. A permitted GET can retrieve a page containing prompt injection, secrets, or a very large payload. Egress authorization does not make the response safe to feed back into an agent.

Exact body schemas are expensive to maintain, so spend that effort where a route concentrates power. For low-risk reads, forbidding a body and constraining query names may be enough. For account changes, deployments, secret rotation, money movement, or destructive actions, validate the fields that identify the object, tenant, amount, environment, and requested transition. If the API offers a narrower endpoint or credential, prefer that over an elaborate local rule.

Approval should describe the resolved action

Record the resolved API call
Each HTTP request appears in the Activity journal instead of disappearing behind one tool name.

A useful approval prompt shows what the trusted executor is about to send after normalization, not the tool call the model proposed. The reviewer needs the credential label, account or tenant, resolved destination, method, path, meaningful query fields, consequential body fields, and redirect behavior.

That does not mean dumping raw JSON into a dialog. Raw payloads hide the dangerous field among timestamps and defaults. Render an operation summary first, then allow inspection of the canonical request. A deployment approval might say that the production-deployer credential will create release 2026.07.24 in the production tenant, followed by the exact host, POST path, and a body diff.

Bind approval to a digest of the canonical action. If the host, method, path, protected headers, or body changes after the click, compute a different digest and require a new decision. This closes a common time-of-check gap where the interface approves one request object and middleware later follows a redirect, adds defaults, or mutates the body.

Choose the reuse boundary deliberately. Session approval may be reasonable for repeated reads with one narrow credential and destination. A credential capable of deleting resources should require approval per call or a much narrower pre-authorized operation. Do not turn repeated prompts into a substitute for scoping. Humans click through noisy dialogs, especially when every card looks identical.

The approval result should be deny by default when required context is missing. An unresolved hostname, an unknown content type, an unrecognized method override, or a body the policy cannot parse is not a low-risk request. It is an action the system cannot accurately describe.

A harmless plan can still become a dangerous call

The failure usually starts with an ordinary task and a request that changes meaning as it moves through layers. Suppose an agent must read one issue and post a short status note. The host grants the generic caller for the session because both operations use the same project service.

The read credential fails on the POST, so the planner selects another vault entry whose description mentions project automation. That token can also administer webhooks. A retrieved issue comment tells the agent to notify an external callback, and the planner submits the callback URL as a status target. The generic tool still has session approval, the credential name still sounds relevant, and the method is still POST. Tool-level permission sees no boundary crossing.

The initial callback responds with a 307 redirect to a private address. A convenient HTTP library preserves the POST body on that status. It may remove an automatically generated Authorization header, but a custom credential header supplied by calling code can survive unless the client handles it explicitly. The request now carries project authority and issue content toward a destination nobody reviewed. Even if the private service rejects the credential, the body may disclose data or trigger an unauthenticated action.

Four coordinated checks stop the sequence at different points. The read credential cannot authorize POST. The automation credential cannot reach an unregistered callback host. Redirects require a new destination decision. Request rules reject a body containing an arbitrary callback destination. No single check carries the whole defense, and the friendly names of the tool and credentials carry none of it.

This failure is why I argue against approving a generic caller once per session. The pattern is popular because repeated approval cards interrupt work, and a stable tool name appears to describe stable risk. It does not. Scope the session grant to a credential and destination with constrained operations, then ask again when any coordinate changes.

The same sequence can fail without a malicious comment. An agent may infer an endpoint from outdated documentation, copy a URL from an error response, or choose a similarly named credential after the intended one returns 403. Those are normal recovery behaviors for a planner. Security design should expect them and keep the recovery attempt inside the original grant.

Canonicalization must happen before the comparison. Decode percent-encoded path segments according to one documented rule, reject dot segments that escape an allowed prefix, normalize the effective port, and decide how the API treats repeated query names. If policy evaluates /v2/issues/%2e%2e/admin as an issue path while the server resolves it as /v2/admin, the policy and server authorize different resources. Reject ambiguous forms rather than guessing how every intermediary will interpret them.

Credential injection must happen after that work and as close to transmission as possible. Build a canonical request, authorize it, bind the approval digest, open the approved connection, then add the secret in the trusted executor. If middleware can rewrite the host, method, or body after injection, either include that middleware output in the authorization step or remove its freedom to rewrite.

Failures also need a closed path. If the selected credential receives 401 or 403, return that result to the agent without automatically trying every vault entry. Credential fallback turns a narrow failure into privilege discovery. The next attempt should name another credential explicitly and pass a new decision with its own authority and account context.

Treat upstream error messages as data too. APIs often echo URLs, operation names, or suggested retry actions. A planner may reasonably use that text to form its next call, but the text did not gain authority merely because it came from an approved host. The next request still faces the same destination and request checks.

Test this behavior as a sequence, not only as isolated requests. Begin with an allowed read, return a crafted redirect or error suggestion, let the planner form its follow-up, and confirm that session reuse does not widen. Then fail the intended credential and verify that the executor refuses silent fallback. These tests catch permission inheritance that ordinary unit tests around a single policy match will miss.

Logs must record the decision and the call

Keep secrets out of headers
Agents name a vault entry while Sallyport keeps its secret bytes inside the app.

Tool invocation logs are too coarse for incident review. Recording that an agent called http_request leaves the central questions unanswered: which credential was used, where the request went, what operation was allowed, and whether the sent request matched the approved one.

Record the session or process identity, tool call identifier, credential identifier, canonical destination, resolved address, method, redacted request summary, policy version, approval identity, action digest, response status, timing, and final outcome. Keep secret bytes and sensitive response bodies out of ordinary logs. For fields needed as evidence, store a digest or encrypt them under separate access controls.

Log denials as well as successes. A run of rejected attempts against private addresses or alternate hosts can reveal prompt injection or a faulty planner before any external effect occurs. Distinguish policy denial, user denial, network failure, upstream rejection, and local cancellation so responders do not mistake a failed connection for a blocked action.

Evidence should be hard for the acting agent to rewrite. Append-only storage, restricted writers, and integrity checks make the record useful after a workstation or process is questioned. Sallyport records agent sessions and individual calls from one write-blind encrypted, hash-chained audit log, and sp audit verify checks the chain offline over ciphertext. It also keeps API and SSH credentials in an encrypted vault and executes actions without exposing secret material to the agent.

Replace the broad grant at the executor

You can keep a generic HTTP interface without keeping generic authority. Move credential injection and enforcement into a trusted executor, then make the model submit an uncredentialed request plus an opaque credential reference.

A practical migration starts by inventorying actual calls rather than imagined ones. Group recent requests by credential, origin, method, and operation. Broad credentials and destinations that never appear together are candidates for separation. Routes that carry several destructive modes in one body deserve their own request constraints or upstream credentials.

Next, put existing traffic into report-only evaluation. Canonicalize each request, resolve its destination, and show which proposed grant would allow or deny it, but do not change execution yet. Review surprising matches. A rule that seems to allow issue reads may also admit exports, deleted records, or another tenant through a shared host.

Then enforce the easiest boundaries first: executor-owned credentials, exact HTTPS origins, no automatic redirects, explicit methods, and denial of private addresses unless a particular integration requires them. Add path, query, header, and body constraints around high-impact operations. Keep an escape path that requires explicit per-call approval and produces a conspicuous audit entry; do not silently fall back to the old unrestricted tool.

Finally, test the boundary with mutations of known-good requests. Change the host suffix, port, DNS answer, redirect target, credential reference, method, content type, tenant field, operation field, and encoded path. Each mutation should either match an intentional grant or fail before credentials are attached. Also test that the bytes logged, approved, and sent share the same action digest.

A tool list is still useful for helping an agent choose sensible operations. It is simply the wrong place to end authorization. Keep the convenient call surface if developers like it, but make every network effect earn permission at the point where credential, destination, method, and request details are all known.

FAQ

Why is a generic HTTP tool more dangerous than a specific API tool?

A generic caller can change its authority through arguments such as the URL, method, credential, headers, and body. A specific tool usually fixes more of those choices, but it still needs enforcement on the final request.

Can I make a generic HTTP tool safe by allowing only GET requests?

No. GET is defined as safe by HTTP semantics, but some APIs put state-changing actions in query parameters, and GET responses can still expose sensitive data or hostile instructions. Bind GET to a credential, destination, path, and allowed query fields.

Should every API endpoint become a separate agent tool?

Separate tools improve descriptions and can reduce accidental misuse. They do not replace request-level authorization, because credentials, redirects, shared routes, and body fields can still change what an operation does.

What should an HTTP tool approval prompt show?

Show the credential label and account context, resolved destination, method, path, consequential query or body fields, and redirect behavior. Bind the approval to the canonical request so any later change invalidates it.

How should an agent supply an API credential to an HTTP executor?

The agent should provide an opaque credential reference, not the secret or an Authorization header. A trusted executor should check the request first and inject the credential only after it passes.

Are OAuth scopes enough to limit an agent's API access?

Scopes limit categories of action, but they do not always bind a token to one destination. Use audience-restricted tokens where supported, and still constrain the destination and request details at the executor.

Should an authenticated HTTP tool follow redirects automatically?

Deny redirects by default for authenticated calls. If redirects are required, authorize every new destination and add credentials again only after that destination passes the same checks.

How do I prevent SSRF in an agent HTTP tool?

Allowlist known schemes and origins, resolve hosts in the enforcing component, reject disallowed address ranges, and verify the address actually used for the connection. Recheck redirects and defend against DNS changes between validation and connection.

What belongs in an audit log for agent HTTP calls?

Record session identity, credential identifier, canonical destination, resolved address, method, redacted request summary, policy and approval data, action digest, response status, and outcome. Log denials too, while keeping secret bytes out.

When should an HTTP action require approval on every call?

Use per-call approval for credentials or operations that can delete resources, change production, move money, rotate secrets, or cross a similarly consequential boundary. Repeated low-risk reads may use a narrower session grant if the credential and destination are tightly constrained.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov