|
1
|
================================================================================
|
|
2
|
PROPOSAL major-security-1
|
|
3
|
Unvalidated client-supplied filename reaches a file-write sink (path traversal)
|
|
4
|
================================================================================
|
|
5
|
|
|
6
|
Branch : 11645a (task #11645), trunk base r16695
|
|
7
|
Files : src/com/goldencode/p2j/ui/client/gui/DragDropHelper.java (sink)
|
|
8
|
src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebDriver.java
|
|
9
|
src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebSocket.java
|
|
10
|
Severity : MAJOR as a defect in its own right; scoped MINOR for THIS diff
|
|
11
|
because the sink is pre-existing and untouched here
|
|
12
|
Category : pre-existing defect, NOT introduced by this branch
|
|
13
|
|
|
14
|
|
|
15
|
1. SCOPE NOTE - READ FIRST
|
|
16
|
--------------------------------------------------------------------------------
|
|
17
|
This is not a defect in the 11645 changes. The vulnerable sink and its two twins
|
|
18
|
predate the branch entirely and are untouched by it; the branch only reworks the
|
|
19
|
MSG_PARTIAL reassembly that happens to be the delivery route. It surfaced during
|
|
20
|
this review and is written up here so it is not lost.
|
|
21
|
|
|
22
|
Recommendation: raise as its own ticket and fix outside this task's branch. Do
|
|
23
|
not bundle it into 11645 - it changes a method signature used by three call sites
|
|
24
|
and deserves its own review and test pass.
|
|
25
|
|
|
26
|
|
|
27
|
2. PROBLEM
|
|
28
|
--------------------------------------------------------------------------------
|
|
29
|
A client-supplied filename flows verbatim into a file-write sink with no
|
|
30
|
normalization, no canonicalization and no containment check, giving arbitrary file
|
|
31
|
write on the host running the spawned web client.
|
|
32
|
|
|
33
|
Verified chain (all in the post-diff tree):
|
|
34
|
|
|
35
|
1. WebClientProtocol.java:2230
|
|
36
|
AppendMessageTask.run() on the last piece calls processChannel(channel).
|
|
37
|
|
|
38
|
2. GuiWebSocket.java:2419-2441
|
|
39
|
messageType == MSG_FILE_UPLOADING
|
|
40
|
fileName = readMessageText(...) <- verbatim from the peer
|
|
41
|
callbacks.saveDropTargetFile(fileName, channel, fileSize)
|
|
42
|
No validation. Also no gating: the handler does not require a pending file
|
|
43
|
dialog or an active drop target. (Contrast chooseFiles / FileUploadInfo at
|
|
44
|
:2383-2394, which the HTTP upload path does consult.)
|
|
45
|
|
|
46
|
3. GuiWebDriver.java:1935
|
|
47
|
DragDropHelper.readDataAndSaveAsFile(tmpDir.concat(name), channel, length)
|
|
48
|
tmpDir is set unconditionally for every GUI client (ThinClient.java:4327 ->
|
|
49
|
AbstractGuiDriver.java:399) and ENDS WITH File.separator
|
|
50
|
(Utils.getOrCreateTemporaryDirectory, Utils.java:4487) - so concat yields a
|
|
51
|
traversable prefix, not a mangled sibling name.
|
|
52
|
|
|
53
|
4. DragDropHelper.java:254-261
|
|
54
|
Paths.get(name) -> Files.newByteChannel(fileToWrite, OPEN_OPTIONS)
|
|
55
|
No normalize(), no getFileName(), no startsWith(tmpDir) check.
|
|
56
|
|
|
57
|
A crafted peer sending a UTF-16 name field of
|
|
58
|
"../../../../home/<user>/.bashrc" writes attacker-chosen bytes to that path.
|
|
59
|
|
|
60
|
Two more sinks share the flaw, both pre-existing:
|
|
61
|
- GuiWebSocket.java:3540 (small-message MSG_FILE_UPLOADING) ->
|
|
62
|
GuiWebDriver.java:1918 -> DragDropHelper.saveFile:214
|
|
63
|
- GuiWebDriver.startDropTarget:1890, which also concatenates an unvalidated name
|
|
64
|
|
|
65
|
|
|
66
|
3. WHY THIS CROSSES A PRIVILEGE BOUNDARY
|
|
67
|
--------------------------------------------------------------------------------
|
|
68
|
The obvious objection - "that is the client JVM the end user already owns" - does
|
|
69
|
not hold for the web GUI driver. The GuiWebSocket / GuiWebDriver process is the
|
|
70
|
spawned FWD web client running SERVER-SIDE: WebDriverHandler authenticates, spawns
|
|
71
|
the client, and the browser is then redirected to that process's embedded Jetty
|
|
72
|
(VirtualDesktopWebHandler:98). That websocket endpoint is registered with
|
|
73
|
GenericWebSocketCreator at the fixed AJAX_TARGET path with NO Origin check
|
|
74
|
(GenericWebServer.addWebSocketHandler:938-955).
|
|
75
|
|
|
76
|
The OS account it runs as comes from WebDriverHandler.java:986-1006: with
|
|
77
|
OS_USER_OVERRIDE set, or with no SSO OS-user mapping, every user's client runs as
|
|
78
|
the single shared DEFAULT_OS_USER.
|
|
79
|
|
|
80
|
So an authenticated FWD *application* user - who normally has no shell and no
|
|
81
|
filesystem rights on the application server - gains arbitrary file write on the
|
|
82
|
server host as that frequently-shared OS account, which also reaches other users'
|
|
83
|
sessions. That is a real boundary crossing.
|
|
84
|
|
|
85
|
Mitigating factors (why not CRITICAL):
|
|
86
|
- It is gated behind authentication; the spawned client only exists post-login.
|
|
87
|
- A stock browser cannot produce a File.name containing path separators
|
|
88
|
(p2j.socket.js:1746 createFileUploadingMessage just forwards file.name), so
|
|
89
|
the trigger needs a crafted websocket peer or injected JS, not an ordinary
|
|
90
|
drag-and-drop.
|
|
91
|
|
|
92
|
|
|
93
|
4. PROPOSED FIX
|
|
94
|
--------------------------------------------------------------------------------
|
|
95
|
FWD already contains the correct pattern - the HTTP multipart upload path does
|
|
96
|
exactly the missing check:
|
|
97
|
|
|
98
|
UploadHandler.java:231-233
|
|
99
|
Paths.get(filename);
|
|
100
|
file = file.getFileName();
|
|
101
|
uploadDir.resolve(file);
|
|
102
|
|
|
103
|
Apply the same containment at the SINK so all three callers are covered by one
|
|
104
|
change, rather than patching each caller.
|
|
105
|
|
|
106
|
Step 1 - change DragDropHelper to take the directory and the untrusted name as
|
|
107
|
separate arguments instead of a pre-concatenated string. This is the crux: as
|
|
108
|
long as the API takes one joined string, every caller is one concat away from
|
|
109
|
reintroducing the bug.
|
|
110
|
|
|
111
|
private static Path resolveInside(String dir, String untrustedName)
|
|
112
|
throws IOException
|
|
113
|
{
|
|
114
|
Path base = Paths.get(dir).toAbsolutePath().normalize();
|
|
115
|
Path name = Paths.get(untrustedName).getFileName();
|
|
116
|
|
|
117
|
if (name == null)
|
|
118
|
{
|
|
119
|
throw new IOException("Illegal upload file name: " + untrustedName);
|
|
120
|
}
|
|
121
|
|
|
122
|
Path target = base.resolve(name).normalize();
|
|
123
|
|
|
124
|
if (!target.startsWith(base))
|
|
125
|
{
|
|
126
|
throw new IOException("Upload path escapes the target directory: "
|
|
127
|
+ untrustedName);
|
|
128
|
}
|
|
129
|
|
|
130
|
return target;
|
|
131
|
}
|
|
132
|
|
|
133
|
Notes on the implementation:
|
|
134
|
- Paths.get(untrustedName) can itself throw InvalidPathException on a NUL byte
|
|
135
|
or an illegal Windows name; catch it and convert to the same IOException so
|
|
136
|
callers see one failure mode.
|
|
137
|
- getFileName() alone defeats "../" traversal; the startsWith check is kept as
|
|
138
|
belt-and-braces and to catch platform-specific surprises (e.g. Windows
|
|
139
|
alternate data streams, drive-relative paths).
|
|
140
|
- Reject or rewrite an empty result and the reserved names "." and "..".
|
|
141
|
|
|
142
|
Step 2 - route readDataAndSaveAsFile and saveFile through resolveInside, and
|
|
143
|
update the three call sites (GuiWebDriver:1918, :1890, :1935) to pass tmpDir and
|
|
144
|
the raw name separately instead of concatenating.
|
|
145
|
|
|
146
|
Step 3 - add the missing gate in GuiWebSocket.processChannel: only accept
|
|
147
|
MSG_FILE_UPLOADING when a file dialog or drop target is actually pending, the
|
|
148
|
way the HTTP path consults FileUploadInfo. Defence in depth - it removes the
|
|
149
|
unsolicited-upload primitive entirely, independent of the name handling.
|
|
150
|
|
|
151
|
Step 4 - decide the policy for a name collision inside tmpDir (overwrite vs
|
|
152
|
unique-ify). Current behaviour overwrites; preserve it unless the team wants a
|
|
153
|
change, but make it explicit rather than incidental.
|
|
154
|
|
|
155
|
|
|
156
|
5. ALTERNATIVES CONSIDERED
|
|
157
|
--------------------------------------------------------------------------------
|
|
158
|
(i) Sanitize in GuiWebSocket.processChannel where the name is read.
|
|
159
|
Rejected as the primary fix: it leaves the sink dangerous for the other two
|
|
160
|
callers and for any future one. Do it in addition if cheap, not instead.
|
|
161
|
|
|
162
|
(ii) Reject any name containing a separator or "..".
|
|
163
|
Weaker than getFileName() (encoding tricks, platform quirks) and rejects
|
|
164
|
names a browser can legitimately produce on some platforms. Prefer
|
|
165
|
normalization to blacklisting.
|
|
166
|
|
|
167
|
|
|
168
|
6. RISK
|
|
169
|
--------------------------------------------------------------------------------
|
|
170
|
Low functionally, but it is a signature change across three call sites, so it must
|
|
171
|
compile-and-run clean on both the GUI and CHUI web drivers. Behavioural change to
|
|
172
|
be aware of: names that previously wrote outside tmpDir (only reachable
|
|
173
|
maliciously) now fail with an IOException - confirm the error surfaces sensibly to
|
|
174
|
the user rather than killing the drawing thread.
|
|
175
|
|
|
176
|
|
|
177
|
7. TEST PLAN
|
|
178
|
--------------------------------------------------------------------------------
|
|
179
|
1. Ordinary drag-and-drop file upload in the web GUI client: unchanged, file lands
|
|
180
|
in tmpDir.
|
|
181
|
2. Ordinary file-dialog upload (both the websocket and the HTTP multipart path):
|
|
182
|
unchanged.
|
|
183
|
3. Crafted websocket frame with name "../../../../tmp/pwned": expect an IOException
|
|
184
|
and no file written outside tmpDir. Verify via the filesystem, not just the log.
|
|
185
|
4. Name with a NUL byte, an empty name, "." and "..": expect clean rejection, no
|
|
186
|
crash.
|
|
187
|
5. Unsolicited MSG_FILE_UPLOADING with no pending dialog or drop target: expect
|
|
188
|
rejection once Step 3 is in.
|
|
189
|
6. Windows and Linux both, since the path semantics differ.
|