Skip to content

Commit f8c8734

Browse files
authored
Harden URI parsing against CRLF injection (#66)
* Reject CRLF characters in URIs * modernize CI
1 parent a0ed347 commit f8c8734

File tree

3 files changed

+63
-32
lines changed

3 files changed

+63
-32
lines changed

.github/workflows/ci.yml

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,50 @@
11
name: CI
22
on:
3-
- push
4-
- pull_request
5-
concurrency:
6-
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
7-
cancel-in-progress: true
3+
push:
4+
branches: [main, master]
5+
tags: ["*"]
6+
pull_request:
87
jobs:
98
test:
109
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
1110
runs-on: ${{ matrix.os }}
12-
continue-on-error: ${{ matrix.version == 'nightly' }}
1311
strategy:
1412
fail-fast: false
1513
matrix:
1614
version:
17-
- '1.6'
18-
- '1'
19-
- 'nightly'
15+
- '1' # automatically expands to the latest stable 1.x release of Julia
16+
- 'min'
17+
- 'pre'
2018
os:
2119
- ubuntu-latest
22-
- macOS-latest
2320
- windows-latest
2421
arch:
2522
- x64
23+
include:
24+
- os: macOS-latest
25+
arch: aarch64
26+
version: 1
2627
steps:
27-
- uses: actions/checkout@v2
28-
- uses: julia-actions/setup-julia@v1
28+
- uses: actions/checkout@v4
29+
- uses: julia-actions/setup-julia@v2
2930
with:
3031
version: ${{ matrix.version }}
3132
arch: ${{ matrix.arch }}
32-
- uses: actions/cache@v1
33-
env:
34-
cache-name: cache-artifacts
33+
- uses: julia-actions/cache@v2
34+
- uses: julia-actions/julia-buildpkg@v1
35+
- uses: julia-actions/julia-runtest@v1
36+
- uses: julia-actions/julia-processcoverage@v1
37+
- uses: codecov/codecov-action@v5
3538
with:
36-
path: ~/.julia/artifacts
37-
key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
38-
restore-keys: |
39-
${{ runner.os }}-test-${{ env.cache-name }}-
40-
${{ runner.os }}-test-
41-
${{ runner.os }}-
42-
- uses: julia-actions/julia-buildpkg@latest
43-
- uses: julia-actions/julia-runtest@latest
39+
files: lcov.info
40+
token: ${{ secrets.CODECOV_TOKEN }}
4441
docs:
4542
name: Documentation
4643
runs-on: ubuntu-latest
4744
steps:
48-
- uses: actions/checkout@v2
49-
- uses: julia-actions/setup-julia@latest
50-
with:
51-
version: '1'
52-
- run: julia --project=docs -e '
53-
using Pkg;
54-
Pkg.develop(PackageSpec(; path=pwd()));
55-
Pkg.instantiate();'
56-
- run: julia --project=docs docs/make.jl
45+
- uses: actions/checkout@v4
46+
- uses: julia-actions/julia-buildpkg@latest
47+
- uses: julia-actions/julia-docdeploy@latest
5748
env:
5849
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50+
DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }}

src/URIs.jl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ export URI,
77

88
import Base.==
99

10+
# Reject carriage return and line feed characters which can lead to CRLF injection
11+
const _CTL = Set(['\r', '\n'])
12+
13+
function _reject_ctl(s::AbstractString, field::Symbol)
14+
if any(c -> c in _CTL, s)
15+
throw(ArgumentError("URI $field contains control characters; see RFC 3986 & RFC 9110"))
16+
end
17+
end
18+
1019
const DEBUG_LEVEL = Ref(0)
1120
include("debug.jl")
1221
include("parseutils.jl")
@@ -82,6 +91,17 @@ function URI(uri::URI; scheme::AbstractString=uri.scheme,
8291
end
8392
querys = query isa AbstractString ? query : escapeuri(query)
8493

94+
# reject control characters in all components
95+
!isabsent(scheme) && _reject_ctl(scheme, :scheme)
96+
!isabsent(userinfo) && _reject_ctl(userinfo, :userinfo)
97+
!isabsent(host) && _reject_ctl(host, :host)
98+
if port !== absent
99+
_reject_ctl(port, :port)
100+
end
101+
!isabsent(path) && _reject_ctl(path, :path)
102+
!isabsent(querys) && _reject_ctl(querys, :query)
103+
!isabsent(fragment) && _reject_ctl(fragment, :fragment)
104+
85105
return URI(nostring, scheme, userinfo, host, port, path, querys, fragment)
86106
end
87107

@@ -149,6 +169,12 @@ https://tools.ietf.org/html/rfc3986#section-4.1
149169
"""
150170
function parse_uri_reference(str::Union{String, SubString{String}};
151171
strict = false)
172+
try
173+
_reject_ctl(str, :uri)
174+
catch e
175+
e isa ArgumentError && throw(ParseError(e.msg))
176+
rethrow()
177+
end
152178
uri_reference_re = task_local_regex()
153179
if !exec(uri_reference_re, str)
154180
throw(ParseError("URI contains invalid character"))

test/uri.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,19 @@ urltests = URLTest[
533533
@test_throws URIs.ParseError URIs.parse_uri("ht!tp://google.com", strict=true)
534534
end
535535

536+
@testset "Control Characters" begin
537+
bad = [
538+
"http://localhost:1337/ HTTP/1.1\r\nFoo: bar\r\nbaz:",
539+
"http://example.com/\rpath",
540+
"http://example.com/\n"
541+
]
542+
for b in bad
543+
@test_throws URIs.ParseError parse(URI, b)
544+
end
545+
@test_throws ArgumentError URI(; scheme="http", host="example.com\n")
546+
@test_throws ArgumentError URI(; scheme="http", host="example.com", path="/a\rb")
547+
end
548+
536549
@testset "parse(URI, str) - $u" for u in urltests
537550
if u.isconnect
538551
if u.shouldthrow

0 commit comments

Comments
 (0)